arc
arc

Reputation: 4691

Many lights with shadows in three.js causes Fragment shader error

Assume to have a scene with a street with many streetlights (more 20), you move an object close by them and you expect a shadow.

The lights, simply

var light = new THREE.PointLight(0xffffff, 0.5, 6.0);

Only the street has .receiveShadow = true and only the car has .castShadow = true (besides later the lights)

example

In three.js adding .castShadow = true to all of the lights causes following error

THREE.WebGLProgram: shader error:  0 gl.VALIDATE_STATUS false 
gl.getProgramInfoLog Fragment shader sampler count exceeds MAX_TEXTURE_IMAGE_UNITS (16).

Luckily in hour scene we only need a few (at max 4) of them to cast a shadow, as most of the lights are out of reach anyway.

I tried to use 2 approaches

  1. Looping through all the lights and setting .castShadow = true or .castShadow = false dynamically.

  2. Adding and removing the lights completely but setting them with no shadow or a shadow.

With both of them I got the same error.

What other approach would work?

Update

@neeh created a Fiddle for it here (to cause the error change var numLightRows = 8; to a higher number). Keep an eye on the error though, there will be another error with too many lights that isn't caused by the same problem

He also pointed out that we see here that a pointShadowMap is created even when not in use. This explains why there is no change with a "smarter" approach. This now is within the GLSL code.

So we are limited by the GPU, which in my case has 16 IMAGE_UNITS but that isn't the case for all GPUs (my CPU actually works fine with more). You can check on your system with renderer.capabilities.maxTextures. But as mentioned we really only need 4.

The problem remains.

Upvotes: 3

Views: 3134

Answers (1)

neeh
neeh

Reputation: 3025

The problem

Yes a new shadow map will be created for every light having castShadow = true (Actually, this is not the case, check this issue). A shadow map is a drawn on which a shadow is computed in order to be blended on a surface afterwards.

gl.getProgramInfoLog Fragment shader sampler count exceeds MAX_TEXTURE_IMAGE_UNITS (16).

It means that your device can send no more than 16 textures per draw call. Typically, the car (street?) on which you'd like to put shadows is 1 draw call.

To draw a object that receives shadows, all the shadow maps should be blended together with the diffuse map. So this requires to use N+1 texture units for one single draw call. (N being the number of lights that can cast shadow.)

If you dig into Three.js shaders, you'd find this :

#ifdef USE_SHADOWMAP

    #if NUM_DIR_LIGHTS > 0

        // Reserving NUM_DIR_LIGHTS texture units
        uniform sampler2D directionalShadowMap[ NUM_DIR_LIGHTS ];
        varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHTS ];

    #endif

    ...

#endif

Check this tool to see how much texture units your browser can handle (Fragment shader > Max Texture Image Units).


The solution ?

Dynamically creating and deleting lights is bad because it's memory-intensive (allocation of a shadow map...).

But, as gaitat said, you can enable shadows only for the nearest lights. Just do the following in your render loop :

  1. Disable all shadows: light.castShadow = false;
  2. Seek nearest lights
  3. Enable shadow for N nearest lights: light.castShadow = true;

Improvement

This algorithm lonely is bad because it allocates one shadow map per light. In addition to be memory-consuming, the rendering would freeze for a bit every time you cross a new light that has no shadow map allocated...

Hence, the idea is to reuse the same shadows maps for the nearest lights. You can deal with shadow maps like this :

// create a new shadow map
var shadowMapCamera = new THREE.PerspectiveCamera(90, 1, 0.5, 500);
var shadow = new THREE.LightShadow(shadowMapCamera);

// use the shadow map on a light
light.shadow = shadow;
shadow.camera.position.copy(light.position);
light.castShadow = true;

You can get the maximum number of texture units with renderer.capabilities.maxTextures. So you can compute the number of shadow map to create based on it but remember to leave some for more regular maps like diffuseMap, normalMap...

enter image description here

Check out this fiddle for a full implementation (only 4 shadow maps are used).

Upvotes: 3

Related Questions