Point lights and performance, revisited

Earlier, I asked about the performance impacts of point lights. At the time, it seemed that things would be OK, but since then I have discovered a number of issues that I don’t know how to solve.

The biggest issue is the impact of point lights and portals - it’s really slow.

Rendering portals entails doing a second render pass before the main render, to generate texture maps which contain the view as seen through the portal. When I first wrote the portal implementation, I discovered that portal rendering slows down the frame rate a lot, unless both scenes (the main scene and the portal view) have identical global parameters, such as the number of clipping planes or whether there is a global environment map. If, for example, the portal view has clipping planes but the main view does not, three.js will recompile the shaders every frame - because the number of clipping planes is a defined constant in the shader code.

I’m guessing that the number of point lights in the scene has a similar effect, although I’m not sure. All I know is that when point lights are enabled, then any time a portal comes into view, the framerate drops to about 1/3 of what it was before; but if I don’t put any point lights in the scene, then there’s only a slight drop in framerate when a portal comes into view.

Note that in my game world, the number of point lights isn’t fixed. Because my engine, like Minecraft, supports world of arbitrary size, pieces of the terrain (which I call “precincts”) are constantly being loaded, cached, and unloaded as the player moves around the world. Precincts can contain light sources, so the number of light sources will change based on the player’s position. However, precincts are fairly large (64 x 64 meters) so this doesn’t happen very often, and the delay only lasts a single frame, so the player doesn’t notice it. But with the two (or more) render passes needed for portals, it happens every frame.

Another issue that was raised earlier is the issue of shadows. As mentioned, enabling shadows for point lights doesn’t work very well if you have more than half a dozen point lights, because each point light generates 6 shadow maps. I figured I could get away with enabling shadows for the directional light only, since there’s only one of those. This looks mostly OK, even at night. The lack of shadows from the point lights is fairly subtle, especially given the limited radius of the point lights.

Night time view:

Where I run into trouble is the case of something like a candleholder or a torch attached to a wall. This looks fine when you look at the wall from the same side as the light source. But when you look at the back side of the wall, this is where it gets weird - you can see the light reflecting off of the ground and the pillars that make up the wall, which should be in shadow.

Note that because this world is partly procedurally generated, pre-baked lightmaps would be hard to do.

1 Like

I have never tried it, but if the portal uses a second renderer … will this cause the main renderer to recompile shaders each frame?

So it turns out it’s worse than I thought. If I try and render a portal from a scene with point lights, into a scene with no point lights, I get an exception:

Uncaught TypeError: Cannot read properties of undefined (reading 'position')
    at StructuredUniform.setValue (three.module.js:17814:20)
    at StructuredUniform.setValue (three.module.js:17814:6)
    at WebGLUniforms.upload (three.module.js:17943:7)
    at setProgram (three.module.js:28257:18)
    at WebGLRenderer.renderBufferDirect (three.module.js:27177:19)
    at renderObject (three.module.js:27807:10)
    at renderObjects (three.module.js:27776:5)
    at renderScene (three.module.js:27698:35)
    at WebGLRenderer.render (three.module.js:27518:4)
    at Engine.render (Engine.ts:460:19)

Direct render might not work as the compiled shaders are different. I was thinking of something like render1 → texture → renderer2.

I’m hesitant to construct additional renderers, since there’s a limit on the number you have have. I already have two - one for the character portraits, and one for the main scene. For portals, you’d have to have a renderer per portal, and there can be more than one portal on the screen at any given time.

It really depends on what that limit is, which I imagine is going to be different for different GPUs. If the limit is, say, 16, that would work because I doubt there will ever be more than 3-4 portals on screen at once. However, if the limit is 4, then that would likely be a deal breaker.

Also, what’s the overhead of additional renderers? For character portraits, the overhead is small since I’m only rendering a single GLTF model. The main scene, on the other hand, is huge, with thousands of objects, lots of custom shaders (ocean waves, terrain mapping, flames animations, particle systems, etc.) - would a second renderer have to have its own copy of all the compiled shaders, vertex buffers, and so on? That seems like a non-starter.

So I figured out a workaround. For each realm I create a fixed number of point lights, 10 in my case. As the player moves around the world, I query the world for light sources, then sort them by distance to the camera, and copy the nearest 10 of them to the 10 fixed slots, updating the properties of each point light. If there are fewer than 10 light sources near the camera, I set the excess lights to intensity = 0.

This means that there are always exactly 10 point light sources from three.js’s perspective. Shaders never get recompiled because of changing lights; and I can render portals from one world into another without crashing, because the number of point lights is the same.

Still don’t have a solution for the lights shining through walls…would love to hear if anyone has a clever/ugly hack :slight_smile:

1 Like

Light through walls: renderer.physicallyCorrectLights=true;
Tiled forward lights: three.js examples

Regarding number of lights, I can offer my solution, as long as you’re okay with it being under license (I’m not going to charge you though). It integrates fairly trivially, there are some shader rewrites and a runtime that needs to be updated every frame. If you’re interested, just drop me a line - I’d be happy to help.

What my solution doesn’t do though is support shadow maps for point lights. So there’s that :slight_smile:

Although you do have a choice between standard point lights and clustered ones, so it doesn’t remove options.

But as @Chaser_Code recommended, tiled forward rendering example can take you pretty far though.

While I appreciate the suggestion, I think it’s probably beyond my level of skill - I’m not really a graphics guru, I’m a game programmer / artist / musician who happens to know a bit of graphics and math :slight_smile:

I’m not really aiming for photorealism in the game I’m building (I’m going for a visual style which is partly art nouveau, and partly retro gaming circa 2002), so I don’t need thousands of lights - just a few dozen at a time. Because my game is not first-person and you can’t see the horizon, only a small portion of the world needs to be rendered, despite the fact that the game world is kilometers in size.

1 Like

You’d be surprised how many games and engines from the OpenGL days do exactly this. :grin: If your game is typically a (semi) top-down experience, then this solution is fine, as long as you don’t exceed the 10 pointlights limit to objects that can be visible at any given time.

I would personally suggest going the deferred rendering route and offload all lighting to post-processing. However, that does open up a huge rabbit hole not many people are willing to dive into :sweat_smile:

P.s: you can query how many point lights are supported on a device, and use that limit instead. I think the typical limit is 15 or 16 or so.