Optimizing Point Lights

Hello there :cowboy_hat_face:

I was thinking about how we can improve current dynamic light limit in three.js. In a small scene my NVidia GTX 960 could only render 50 point lights before dropping frames. I chose point lights for this as its the most usual case for large number of lights. The simplest method seems to be range check. Currently in Phong material every shaded pixel will iterate over every light to calculate it’s contribution and all materials share the same lights list uniform. Its a naive method and checks for lights which have no chance to be affect the pixel at all.

Forward+ rendering uses (usually) compute shaders which we dont have yet (coming in WebGPU). Another option is to do bounding sphere to light’s range distance checks on the CPU to cull lights, but its a lot of work :smiling_face_with_tear:, so I started with the simplest GPU checking. It’s not as optimal due to code branching, but it has information of pixel position and light position/distance, making this check very accurate (unlike bounding spheres which are only an approximation).

Currently #pragma_unroll is used for looping over lights, but afaik it doesnt offer breaking specific iterations, so I just changed it to dynamic for loops without unroll and did something along the lines of

for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {
  if ( distanceFromPointLightToPixel > pointLight.distance ) { continue; }
  ... // continue with shading and lighting calculations

to break the loop iteration for point lights which are not in range to affect the fragment.
This has caused a success of point light limit increased by 60%! - from max 50 to max 80 point lights. However GPU usage is still extremely high and in a real app/game there would be a lot more going on.

Is this an increase even worth reporting to github? It ain’t much, but requires very little shader chunks changes. What do think?

Here is the scene with 80 lights:

Actual shader code changes is just 2 tiny chunks edits:

  1. THREE.ShaderChunk[ 'lights_pars_begin' ]:
    128: light.color = pointLight.color;

    light.color = pointLight.color * step( lightDistance, pointLight.distance );
    This makes the light color black if the pixel is outside the range of the point light. This 2 lines later marks the light.visible to false

  2. THREE.ShaderChunk[ 'lights_fragment_begin' ]:
    38: #pragma unroll_loop_start
    39: for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {

for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {
	pointLight = pointLights[ i ];
	getPointLightInfo( pointLight, geometry, directLight );

	if ( !directLight.visible ) { continue; }

which prevents further lighting calculations done inside
RE_Direct( directLight, geometry, material, reflectedLight );
thus saving some performance when there are a lot of distance-limited lights. I tested in the editor and it seems like the point light distance is always accurately respected, regardless of renderer.physicallyCorrectLights