How can I optimise my THREE.JS rendering?

You cannot expect this from threejs. It does the best it can, but what it can or cannot see is pretty application dependent.

Well comparing my original code to my latest one I am only doing 2 things different and it has significantly improved my performance, those are:

  1. Not drawing “walls” that are 0 in height (these exist for a specific purpose, not my map design spec).
  2. Reducing the FAR plane distance of the camera.
    I may run into more issues when I can get around to texturing but at the moment the coloured walls are using more draw calls than I expect but seems like I may be hitting the limitations of THREE, so as it is now “smooth” I’ll stop tinkering with it.
    Thanks for all of your suggestions.
1 Like

Actually, there are two - and big - problems, and it’s not just about draining batteries, they are the least concern. The first problem is running the loop pointlessly, it doesn’t make sense, it’s like paying a lot for electricity because you keep the lights on when it’s day outside. The second is that even though you set the rotations to 0, rendering and the zero rotation, as well as the other processes you might have in the loop, still happen, taking CPU and GPU power and usage (and potentially heating them up and triggering the fans) that you might need for other applications.

Yes, rendering at a certain rate is fine, and not rendering some frames doesn’t make the others run faster, it’s just that there is no reason - and a bunch of reasons against - to render frames when the user doesn’t need them. If you set your rotations to 0 it goes without saying that you’re looking to pause the animation and, I don’t know, perform it on demand or something similar. So, it’s wise to stop the requests for frames - or, alternatively, add the loop function contents in an if {} else {} condition that only performs resource intensive stuff when needed - instead of being lazy and ineffective and setting the rotation increments to 0.

Simple example: in my 3D globe project, when I run the animation / simulation (similar to Celestia, if you’re familiar with that), I go from 3.5% CPU and 8% GPU to 5.5% CPU and 40% GPU. That might not seem much for my current laptop specs and is fine when the user is actively looking to run it and all that, but it’s a clear unnecessary drain of system resources if the user wants to pause things and, for example, do some video encoding, play a game, or any other more intensive activities in the meantime. My setup allows the “page” to be embedded like an interactive animated “wallpaper” in a desktop application (Rainmeter) that always stays on screen and in the system tray, so additional resource usage for no reason does make a difference, with other applications (like the AV, browser, maybe some YT or playing a movie) in the background.

Don’t worry, expecting efficiency and lower reasource usage from any application (or library, for that matter) is perfectly reasonable, irrespective of what system you run them on. The fact that some folks or companies nowadays are not that interested in it doesn’t make it any less valid. This is what is important and matters to the user and should be ingrained into the “best coding practices”, not formatting the syntax a certain way, using const instead of var or other such nonsense with minimal impact on things.

I had a somewhat similar situation to your wall situation in my project. I use a customized public video to render animated clouds on my globe, and had to use seeking iin the video instead of playing it to allow playing backwards and full control over the speed of playing. The problem was that it took 12% CPU to do that at the same time with the main animation in the animation loop, but as soon as I re-encoded the video to turn all its frames into keyframes and only seeked into the video when such a frame needed to be played (as opposed to all the time), the usage magically dropped to 6% or less, being even better than the alternative playing method. Sure, when speeding up my main animation, more frames need to be played from the video during a second as well, so the usage raises to 9% at most, but those are rare and usually improbable situations, since you can’t see much on a crazy spinning globe anyway.

I also support the view of the other guys here that you should share the code that is producing the issue. I perfectly understand that it’s hard to simplify in order to post it, or has other dependencies that those who help don’t have, but as a reference to base the help on, it’s surely useful. It’s nice when the code is simple and is in a fiddle, we all like that, however when it’s not you work with what you got.

The issue with sharing the code is that I can’t share the data as it is from a paid for game and I have done some things to extract the data so you can’t just supply the data as is. The code won’t run without the data.

Ah, ok, that’s fine with me. I have to say though, now that you mentioned it, how correctly or how much of the “data” was extracted, or some other particularities related to that or the environment it’s supposed to run within, is something that could be considered as a potential cause for such issues. Hard to tell without seeing anything, but if it’s not a built from scratch thing, other processes in the said environment could have an impact. It’s not a certainty, but personally, I would add it to the list of possibilities.

It is fully extracted as the map files are inside one big file along with the art work, sound etc, the maps themselves are just as single file with the geometry, imagine an OBJ file if you will without the MTL.
I am colouring the planes myself but building the planes with the geometric line data from the map file so it basically outputs “start x, y”, “line to x, y” and then completes the polygon for the floor / ceiling, then I use each “line to x, y” to render the wall by referencing the next line in the list and the last one loops back around to the first x, y. Each map file can contain multiple “rooms” each with their own set of x, y “walls”, I am just iterating over each room / wall creating the floor / ceiling shape producing a mesh then reiterating over each room / wall again to generate the walls. The data is fully parsed into JS objects before passing it to the renderer so it isn’t “loading” while it renders, it has all of the data beforehand.

Yeah, well, I don’t know what else to say, it looks like you handle things properly, however that issue happens for a reason…

Yes, maybe it is a limitation of the current implementation via JS, who knows, but it works better now, thanks for all of your (and others’) suggestions, I think explaining the problem via text has helped me see what else might be a potential solution (like the far plane distance).

No problem, glad to help as much as possible considering circumstances. Other folks pointed out some useful things as well. :wink:

I think that this is a very bold statement. All of the implementations that i’ve seen that render on demand are cumbersome and complicated, if they even work correctly at all. So i a good reason for is KISS.

Your example is pretty specific and sounds like it only has user interaction and no animations. Three can indeed be used to make your project that specifically handles rendering in some way, or it can be used to make a game or a continuous animation. This in turn, even if it had your optimizations applied, would still ask for every frame…

Hence, there is a very valid reason to do both in three, except that just having a loop is (objectively) simpler.

I think threads should take care of your cpu load, most of the pages are going to run in a single thread.

Exactly my point. So i hope this is clear now:

Why were you rendering a “wall of height 0” at all in the first place? You should have culled those. But it’s not frustum culling, it’s not fancy occlusion culling, it’s just if this rectangle actually has no surface, and is thus not even a rectangle, i don’t want to waste resources drawing it because i know it wont draw anything… cull.

I feel that this thread can be dangerous, because it may be used by other people in the future, yet it feels that it contains some really weird and wrong conclusions.

1 Like

It may be helpful to come up with a mock of the data. Something that’s simplified but aims to mimic the original.

It was just an edge case that I forgot about initially, when the data is read in there are walls with a height of 0 that are for some reason there and are at a far out X,Y Coord, so you would never “see” them, and whilst in a map with 2000 odd walls there were perhaps 5 or 6 with no height it didn’t make much difference to the performance, if at all. I know it is not frustum culling or anything it now just doesn’t even draw them, they don’t exist in 3d space. Technically though a wall with no height can still make up part of the floor / ceiling shape so it isn’t all useless.

For anyone else reading this thread I would say the main takeaway was that reducing the cameras ar plane from 1000 to 100 worked wonders, obviously if you can instance or merge geometry then you should but that wouldn’t work in my case.

Yes, I could make some mock data, and use my stand alone test code (which has the same rendering code), I would have to move some of the later changes to it and create the data and i’d have to write something that extracts the data from the map and puts it into an array, so when I get chance i’ll do it.

That’s a very suspicious conclusion since changing the distance really shouldn’t affect rendering that much.

:frowning:

Maybe it’s better to start over. If you are rendering an old dos game - which can easily be 30 years old, you shouldn’t have trouble rendering it on a modern machine, using hardware acceleration.

Así may be worth mentioning, you could still post the code without the data, and people could see if you are doing something odd.

Can you try just merging your entire map once you build it? Just iterate through every geometry, clone it, apply the world matrix transformation to it and put the results in a new geometry. You should get one draw call, and things should be blazing fast. To me the biggest suspect here is depth testing, but I doubt you turned that off.

Something like this perhaps:

const walls:Mesh[] = [...]
const vertices:number[] = []

walls.forEach(wall=>{
  const tempGeom = wall.geometry.clone()
  wall.updateMatrixWorld()
  tempGeometry.applyMatrix4( wall.matrixWorld )
  const {array} = tempGeometry.attributes.position
  for ( let i = 0 ; i < array.length ; i ++ ){
    vertices.push(array[i])
  }
})

const mergedGeometry = new BufferGeometry()
mergedGeometry.setAttribute('position', new BufferAttribute(new Float32Array(vertices),3))

const mergedMesh = new Mesh(mergedGeometry)

scene.add(mergedMesh)

The end result should be your entire level map, properly arranged in space, rendering with one draw call, blazing fast. From there you can go back and see what was causing these weird issues.

Bold doesn’t hurt if it’s the truth, you know, and there is no danger in sharing one’s experience with a certain system, even though others might disagree. Just because most of such implementations are cumbersome, complicated, or don’t work doesn’t mean any similar one is too. I have both user interactions and animations - otherwise I’d have no reason to mention requestanimationframe() in the first place. Sure, it’s not the Three.js specific animation system based on mixers, clips and such, however it’s a common JS way of doing it.

I animate the globe, for example, in five possible ways based on various combinations of requestanimationframe(), setTimeout() and setInterval (automatic animation, no interaction needed), most of them used for comparison purposes, as well as allowing the user to perform singular actions on demand (not exactly, since the step is bigger, but the equivalent of going “frame by frame”) - and I seamlessly alternate between the two on mouse click, depending on context. In parallel with the globe animation, I also “playback” the cloud video by loop seeking to the right video frame corresponding to the globe’s rotation, and while this isn’t technically using specific animation functions, it does produce the same effects. I could share a brief example as there are just a couple of lines calling other functions, but it’s outside the scope of this thread.

I guess we’d have to agree to disagree here, even though I find it curious that one one hand you advocate letting unneeded frame rendering go on when the scene is more or less idle, and on the other hand you have a bold stance similar to mine when it comes to not wasting resources to draw zero height walls below. From my point of view, both situations are about the same thing: not do something intensive unless that something is needed (by the logic) or requested (by the user). It doesn’t matter if it’s about animations or walls, the principle is the same - correct me if I’m wrong.

As for pages asking for every frame, it’s the browser that does that, with the GPU rendering it on the monitor according to its refresh rate. All I’m saying is that there is no point in adding more workload to that process by transforming and rendering 3D animation when the scene is static (i.e. when there is nothing going on in the scene in terms of visual changes).

My point is, if there’s any more code than this, it’s optimization:

const animate = ()=>{
  requestAnimationFrame(animate)
  render()
}