Make SpotLight shadow camera fov smaller than light angle

Apparently it is too difficult to make the fov of a SpotLight’s shadow.camera be different than the angle of the light.

The fov of spotlight.shadow.camera will be overwritten based on spotlight.angle, and doing this is not enough to make it work:

	light.angle = (100 / 180) * Math.PI

	light.shadow.camera.fov = 10 // we want a custom value

	const originalUpdateMatrices = light.shadow.updateMatrices
	light.shadow.updateMatrices = (...args) => {
		originalUpdateMatrices.call(light.shadow, ...args) // this sets light.shadow.camera.fov based on light.angle

		// attempt to restore the shadow camera fov to a custom value, no luck
		light.shadow.camera.fov = 10
		light.shadow.camera.updateProjectionMatrix()
	}

It’s a bit annoying that the spot light’s shadow camera’s fov is so difficult to configure (without changing the light angle).

Even if that code worked (it doesn’t), it is not ideal to have to keep calling updateProjectionMatrix in excess.

What can we do?

One trick is to set light.shadow.focus to a portion (0 to 1). This will be the portion of the spotlight angle that the shadow camera will use for its fov.

For example, set light.shadow.focus = 0.5 to make the shadow camera fov angle be half of the light angle.

But still, why does overriding the fov like I did not work. Hmmm.

I thought a spotlights .camera fov is intrinsically tied to the lights “angle”?
like.. changing the .angle of the light is expected to update its .camera.fov

What does changing its camera fov directly, achieve that you can’t achieve by setting light.angle?

Hard to tell without trying, except noticing that you could try the full update after changing fov. After(!) camera.updateProjectionMatrix there is super.updateMatrices:

Also note, that the camera fov is not primary (source) data, but derivative (calculated) data – see lines 43 and 49. The primary data are angle and focus. Usually changing derivative data has no effect, unless the subsequent calculations are redone, better change the primary data.

4 Likes

What

When you have spotlight with angle bigger than what it is pointing at, you can reduce the shadow camera fov to make the shadow more detailed:

Here’s a picture (yellow cone represents light angle, gray cone represents shadow angle (fov), and the shadow of the person is gray):

The smaller shadow fov results in using more of the shadow map for the shadow, so the shadow will be less pixelated (i.e. the shadow will be finer in detail).

If there’s nothing else that needs a shadow within the spotlight angle, leaving the shadow at full size of the light angle is potentially wasting the shadow map (much of the shadow map will be unused).

Why?

If you have a scene with a point light and a single subject, and you want shadow on the object, and the camera is looking at a similar angle as the light towards the subject, a point light will be very wasteful: the point light will render the scene with 6 different direction shadow cameras (basically a cube map, 6 shadow maps) to calculate shadows in all directions.

With a spot light, we can reduce this to a single render of the scene (a single shadow map) to achieve the same effect (not only at a reduced cost, but with better quality).

Point light shadow radius 0, map size 512:

Point light with shadow radius 3, map size 512

Spot light with shadow radius 0, map size 512 (not only looking better, but notably faster than point light):

Spot light with shadow radius 3, map size 512 (not only looking better, but notably faster than point light):

With a spot light for this scenario, we can even turn up the map size and still be faster than point light:

Spot light with shadow radius 0, map size 2048 (not only looking better, but still notably faster than point light):

I wanted the softer look with the 512 shadow map size, so win win.

Note, for some reason spotlight.shadow.radius seems to have no effect with spot lights, only point lights, however reducing the spot light map size happened to give the blurred effect I was after. This seems like a bug.

That is true. That’s why in the OP I tried to override the logic to force the fov to be applied with my own values instead of it being derived from angle.

1 Like

@Mugen87 can you confirm if this needs fixing? I can set spotlight.shadow.radius from anywhere between 0 and Infinity and nothing happens.

Maybe for spot lights shadows at the edge/penumbra need special treatment, but that’s not a requirement in my use case. If spotlight has such special behavior, maybe a special value like -1 could be the default to enable special behavior (or some other property unique to SpotLight), that way we can use radius for other use cases like mine above.

EDIT: Ah yep, here are the calculation that SpotLight uses with PCFSoft shadows are used, which does not include radius in its calculations:

And here are the calculations that PointLight uses with PCFSoft shadows, which includes radius:

This is a bit confusing. Maybe we can fix this. Are you open to the idea of being able to enable the radius behavior in a certain way, or permanently enable it assuming the default is 0. F.e. it could be spotlight.shadow.applyRadius = true or spotlight.shadow.radius = 3 (instead of 0 or -1), or something along those lines.

Another question is, why do non-point lights not have radius applied in all cases like point lights? I’d like to make things more intuitive (f.e. setting radius but nothing happening is not intuitive).

Oh, doh! Yep, because super.updateMatrices() is needed to be called again, which is missing in the patch of my OP.