R168 WebGPU - Chasing Shadows - fixed in r169

R169 PERFECT COCKPIT SHADOWS!!!

SETTINGS:

let sunLight = new THREE.DirectionalLight("white",3);
	sunLight.position.copy(SunPos); // (-34.2,19.5,-30.8)
	sunLight.castShadow = true;
	sunLight.shadow.camera.near = 0.001;
	sunLight.shadow.camera.far = SunDst+ShdDst+ShdBox; //50+1500+5
	sunLight.shadow.mapSize.width = 8192;
	sunLight.shadow.mapSize.height = 8192;
	sunLight.shadow.camera.left = -ShdBox; // -5
	sunLight.shadow.camera.right = ShdBox; // 5
	sunLight.shadow.camera.top = ShdBox; // 5
	sunLight.shadow.camera.bottom = -ShdBox; // -5
	sunLight.shadow.bias = -0.00001; // determined by trial and error
	scene.add(sunLight);

	renderer.shadowMap.enabled = true;
	renderer.shadowMap.autoUpdate = true;
	renderer.receiveShadow = true;
	renderer.shadowMap.type = THREE.PCFSoftShadowMap;

Using mapSize of 8192 and setting ShdBox to the absolute minimum made the biggest difference. Any lower mapSize or larger ShdBox and the shadows started getting fuzzy or distorted. It runs fine at 60fps.

ORIGINAL POST
When updating my WebGPU programs from r167 to r168, I ran into a problem where the programs either ran slowly or not at all. After some trial and error, I discovered that the cause of the problem was my shadow mapSize. After reducing the mapSize from 8192x8192 to 2048x2048, the problems disappeared and the programs now appear to be running fine, except for a loss of shadow detail…

This may be a problem only for people (like myself) with an older GPU.

NOTE: I was wrong. On my most complex program, shadows are significantly hurting my frame rate. The only way to get back to 60fps is to remove shadows entirely. So there has definitely been some kind of change to shadows with r168.

A FINAL NOTE:

Thanks to everyone who contributed. I have learned a lot about shadows and more!

I was able to get my programs to working at 60 fps again by making one additional change: I reduced the number of objects which received shadows. To explain: my programs create a large surface (the Ocean) using nested scrolling grids which contain individual planes: the inner grid contains the smallest planes, the middle grid contains larger planes and the outer grid contains the largest planes. For r167 and earlier, I had set “receiveShadow = true” for all of the planes in all of the grids, even though my object would never cast a shadow on the planes in the middle or outer grids. The program now sets “receiveShadow = true” only for the planes in the inner grid.

The two changes - reducing shadowMap size and reducing the number of objects set to receive shadows was enough to get things working again. The usage meters still shows that, even with these changes, r168 has increased the GPU usage and there is an odd sawtooth pattern that wasn’t there with r167 I expect that this is an unplanned result of changes made to shadows in r168 and will be adjusted in future revisions.

A FINAL FINAL NOTE
The problems with shadows appear to have been fixed with r169. No frame rate drop and no spikes in GPU usage. I am experimenting with different bias settings. The old standard of -0.00005 appears to work pretty good. Also, the renderer shadow commands have been re-activated, so leave them in. Fingers crossed!

An 8k shadowmap is considered huge for most scenarios.
I have rarely seen scenarios that use >2k shadowmaps…

The way to think of shadowmapping is that it is basically re-rendering the scene to a display size of 8kx8k… just for the shadow…
It’s a lighter form of rendering since it only has to render the final depth, but it’s still not a cheap operation, if you think about trying to render a 3d scene to an 8k monitor (do those even exist?) you can see how this would be expensive. I would expect the rendering cost to be roughly + .5x the normal cost of your frame rendering.
So if you’re getting 50fps without shadows, and your shadowmap is roughly screen sized, I’d expect fps to go down to ~20-30fps

If the resolution is too low, you may need to adjust the coverage area of the light itself, by changing its camera.left/right/top/bottom to concentrate the shadowmap over a smaller area.
If that’s not enough to get the density that you want, you might want to look into CSM or VSM shadowmapping.

r.e. - the performance regression, that could definitely be in play… perhaps some recent change has changed the complexity of shadowmap rendering, making it more expensive… but I’m sort of amazed that you had 8k shadowmaps before and they weren’t already tanking your performance.

2 Likes

Thanks for that explanation. So shadow generation uses an ortho-camera located at the position of the light? I am using the directionalLight as a proxy for the sun in a large landscape.

In that context, I have a couple of basic questions:

  • Is the shadow camera pointed at 0,0 by default, or do I need to specify that?
  • What is the correct/most efficient distance for the light? I have tried distances ranging from thousands of meters to a normalized value. If I use larger distances, I need to increase the far distance accordingly.
  • I had the near distance at 0.1, but that may make no sense for a distant camera (I assume that means “near” the shadow camera?).

The camera improved vastly with r167, especially with WebGPU and NodeMaterials. But now it seems to be misbehaving. It appears that they did work on WebGPU shadows for r168, so perhaps some unintended change was made.

I think you are really pushing strongly for that “my gpu is too old” narative :slight_smile:

My understanding is that WebGL was at least a decade behind what hardware could do. So if webGPU finally allows you to use that hardware, even older graphics cards should become more powerful.

I wonder if this is the correct conclusion.

2 Likes

You can use DirectionalLightHelper to visualize the light, and if you make the ortho camera be the same size as the helper, then you will visually see where the frustum is and where it is pointing.

With this technique, you can probably reduce your shadow map size to 256x256 (just hand waving a number, it depends on how big of an area on screen you want shadow for) or even smaller depending on your scene, making the shadowmap frustum tightly encapsulate your object, and the shadow will be crisp and much faster.

1 Like
  • shadow camera points at the DirectionalLight.target object by default which is at 0,0,0
    If you move the directionallight, you also have to update its target object.position.

  • It’s less the distance to the light that matters, but the distance between shadow.camera.near and far. You want that distance as small as possible, yet still providing coverage for the scene.

  • Setting near to .1 is fine, its the distance between near and far that matters most.
    If you have a near of .1 and a far of 10,000, the numerical range of shadow values is 10000-.1
    so 100000. This range is what is going to be squashed into the range of the shadowmap depth texture. If that range is large, you will get more shadow acne/less precision on shadows.

If you need robust shadows for an open world type scenario, you probably want to use a more advanced shadowmapping technique than straight PCF… like CSM (cascaded shadowmaps)
https://threejs.org/examples/webgl_shadowmap_csm.html

or

VSM
https://threejs.org/examples/webgl_shadowmap_vsm.html

r.e. how it works in webGPU, that entire pipeline is being actively developed/debugged, so i am not surprised if you are hitting issues that change between releases. Porting all these shadow models and behaviors to the new rendering backend is an absolutely heroic task. Well underway, but definitely still going to be churning for a while.
This is the main reason that I am holding off on using webGPU for anything big yet.
It’s called the bleeding edge for a reason. :smiley: I’m not saying this to discourage your efforts, just to adjust your expectations. The work/experimenting you are doing is absolutely vital and useful to making this transition happen, and you have my gratitude and respect.

1 Like

I think that WebGPU and NodeMaterials are such an improvement that I am willing to endure a little pain in the interest of progress. :grinning:

For now my projects focus primarily on a single moving object located at 0,0, but that sometimes casts a distant shadow - as far as 3000 units. I have been working at a scale of 1 unit = 1 meter in a world that has is visible out to 100k meters.

But your point that the distance between between far and near is so important makes me wonder if I should consider “miniaturizing” my world, e.g 1 unit = 1000 meters. But I suspect that would screw up the computation of perspective.

1 Like

That’s a good idea. II will give it a try, just in case I am missing something.

But, in this case, I found that reducing the mapSize further does not result in an increase in fps. Just guessing, I would suspect that r168 moved the map to a place where my PC does not have the memory to handle an 8192x8192 map - most likely my GPU.

Similarly, reducing the distance between the far and near values has no effect.

So it is a bit baffling.

I will know soon. I have a new GPU on order. But I am aiming for a “middle of the road” setup because I want my programs to be usable by others.

1 Like

Ok… so… shrinking the world wont actually help. because by shrinking the world, you now need more resolution to correctly shadow the finer details…

Ideally you want the shadow camera left/right/top/bottom/near/far to describe a box that covers the bare minimum that you need to have in shadow…

this can even be adaptive… for instance maybe in an indoor scene, you tween the far value in tighter, to get cleaner shadows indoors… (probably overkill) but. yea… you get the idea.
Cover the minimum region possible that you need to shadow, with the shadowcam. (within reason and what you have control over… and if that isn’t enough to prevent shadow issues… consider vsm/csm etc.)

1 Like

If you change those left/right/near far, you have to call shadowcamera.updateProjectionMatrix() for it to take effect.

1 Like

Thanks for all of this information. There don’t seem to be a lot of tutorials (if any) regarding shadows and how they work.

Before using shadows, I used to use a semi-transparent black dot to represent an airplane shadow and that worked okay. If I had wanted to get more detailed, I could have turned the dot into the flat shape of the airplane casting the shadow. That worked great when over a flat terrain. At altitude, you could even raise the shadow dot several meters above the terrain and no one would notice. But it does not work as well as the three.js shadow over uneven terrain.

Shadows were not an issue on fixed objects. Since the sun does not move, I can simply paint the shadow on the terrain.

The place where three.js shadows were really useful was on the vehicle itself: where parts of the airplane cast shadows on other parts, both on the outside of the airplane and in the cockpit. And these have gotten a bit worse with the changes to WebGPU. They got a lot better with r167, but got a little worse with r168. In both cases, I needed to make changes to the bias setting to get them to work great. This is a problem that I expect (hope) will go away as WebGPU improves. Like you say, the three.js programmers deserve our thanks (and patience) with what they are doing.

But, as you can see, in my particular case, I generally only need two sets of shadows. The shadows on the airplane and the single shadow the airplane is casting. And they can be a good distance from each other. (IRL an effective way to spot a nearby airplane is to look for the shadow it casts on the ground.) So I wonder if the programmers could efficiently split these tasks into two parts so that the program does not have to deal with the large distance between near and far?

Could airplane shadow on water be made by a ghost image of the plane near the water surface? The ghost should be on the vector from the sun to the water through the airplane. The ghost could be a flat silhouette, facing downwards thus, invisible when looking from above.

The silhouette does not need to be perfect.

2 Likes

Love wet zone! Under water refraction of the shadow makes so much sense with your proposal :melting_face:

1 Like

That’s something I had not considered!
How would it handle mountainous terrain?

1 Like

I have not added a GPU usage meter to a couple of my simple r167 and r168 programs. These have identical shadow setups with a shadow map size of 2048. Both run at 60 fps, but the difference in GPS usage for the r168 version is significant.

I am not sure if this is a temporary glitch or an intentional effort to assign more work to the GPU.

Maybe casting a ray & moving the ghost airplane some distance away from the intersection point?

It would be more fun to have clouds/fog with a glory effect.

2 Likes

Yes, the “glory” effect is a rainbow, which appears opposite the sun (like all rainbows) and is a circle. I have seen that many times when flying a small plane. But you generally just see a shadow on the clouds. I would love to be able to recreate all those effects in three.js.

1 Like

How about this as an extension to the idea… three.js webgl - lights - spotlight

You could put the image of the plane through a spotlight map with negative values and then project this on to the ground below the plane :thinking: this would work over mountainous terrain…

1 Like

But doesn’t a shadow cast by a spotlight change size with distance? How would you prevent it from changing size?

1 Like