Understanding HemisphereLight

I know this isn’t the “normal” way to use a HemisphereLight, but I’m not trying to do something normal…

I’m trying to use a DirectionalLight as my key light, and the HemisphereLight as a fill/backlight analog. This seems easy enough, but my brain is stuck trying to get it to follow the camera (I move the camera rather than the scene to give the appearance of rotation, pan, etc.).

The DirectionalLight is easy: I add it to my Camera rather than the Scene, and the light follows the camera, and updates its direction as if it lives in camera space. Wonderful!

When I place a HemisphereLight directly in my Scene, it works as expected. I can even adjust the position property to get different effects. So far, so good.

The problem I’m having arises because I want the entire light system to follow the camera. The HemisphereLight behaves differently if I add it to my Camera: Only the “sky” part of the lighting affects the shapes.

Here is a raw example of what I’m seeing with only adding the light to the camera and no other tweaks:

I have tried adjusting the HemisphereLight.position property to point in various orthogonal directions, but the lighting effect doesn’t change. I’ve tried converting the position into world coordinates and fiddling with those, but still no change to the lighting effect.

The final effect I’d like to achieve is the HemisphereLight acting as a kind of 3/4 fill light, as if it’s shining from over the opposite shoulder of the key light. Any ideas with how I can achieve this without needing to put it directly in the scene and constantly update the position property with a new up vector from the camera?

Some further background in case there’s a better way to do this…

The reason I went with HemisphereLight is because it doesn’t cause a specular effect. I do still want a specular effect! That’s why I still have the DirectionalLight. I just want to fill the scene with more non-ambient light sources without causing unwanted shine.

HemisphereLight similar to AmbientLight are actually no real light sources. This is probably the point where many users struggle.

A DirectionalLight emits light into a certain direction. The direction is computed based on the light’s position in world space and the target position in world space. Sidenote: It was considered to remove targets at some point and derive the lights direction from its actual rotation.

HemisphereLight and AmbientLight are actually simplified light probes. A light probe measures light that travels trough a certain position in 3D space. It can compute for arbitrary surface points the respective irradiance (the light that hits the surface). The only input you need for this computation is the light probe itself and the surface normal (which expresses the surface’s orientation at a specific point).

AmbientLight is the simplest version of a light probe because the surface normal does not matter in this case. The same indirect diffuse light hits the entire surface.

HemisphereLight is a bit more complex since you can define two colors values (sky and ground color). The surface normal is compared with the axis of the hemisphere light to determine the final irradiance. The thing is that hemisphere lights just have a position and no orientation. The axis is derived from its position. E.g. if you place it at (0,100,0) the axis in world space would be (0,1,0). It does not matter if you rotate the light.

That’s how I understood it, too (and I was actually looking into light probes before I decided it would be a lot of hassle for what I was trying to do).

I think I found the problem. When I debug, I see everything working as expected. But when it renders, the HemisphereLight.matrixWorld is getting updated. The code in WebGLLights.js indicates it’s pulling the direction uniform from the light’s matrixWorld, so it now makes sense why the light’s derived axis is off.

I tried setting hLight.matrixAutoUpdate = false;, but that didn’t seem to have an effect. The auto-update process in the render must be ignoring that attribute?

I swapped out hLight.updateMatrixWorld with a Proxy and was able to break into what was updating it. Apparently in my version of three.js (I’m currently locked to r97, and I don’t know if this is still the case), scene.autoUpdateMatrix being set to true causes a cascade that forces the updates through the entire scene, regardless if a node has the flag set to false.

This actually shouldn’t be a problem in my live application because I manually handle matrix updates. I’ll have to move my experiment over there and see what happens…