CesiumGS / cesium

An open-source JavaScript library for world-class 3D globes and maps :earth_americas:
https://cesium.com/cesiumjs/
Apache License 2.0
12.76k stars 3.46k forks source link

Make Models be affected by Fog #4196

Closed laurensdijkstra closed 9 months ago

laurensdijkstra commented 8 years ago

glTF Models are currently not affected by Fog. This creates a highly unrealistic image. It would be desirable to have Models be affected by Fog just like the Terrain Primitives are.

ptrgags commented 9 months ago

This feature has two parts: fog culling (tiles in the distance for horizon views should use lower-resolution tiles) and fog rendering (Blend the model color with the atmosphere color like terrain does.

Fog culling is already implemented in CesiumJS, but not enabled by default. It goes by the term dynamicScreenSpaceError. I find this a bit confusing, and the documentation could be written in a clearer manner.

This is an unfamiliar part of the 3D Tiles code for me, so I explored the code a bit yesterday and now can explain the parameters better:

Parameter Effective range of values Description
dynamicScreenSpaceError {true, false} The switch that turns fog culling on/off.
dynamicScreenSpaceErrorFactor >=0.0 px Fog culling works by reducing the computed screen space error (SSE) in pixels for tiles far away from the camera for horizon views. This factor is the maximum SSE adjustment. For example, if this is the default of 4.0, then tiles far away from the camera will be SSE = tile.SSE - 4 px. Increasing this value is the easiest way to increase the intensity of the fog culling's effects. Setting this to 0.0 has the same effect as turning off fog culling. Furthermore, this should be restricted to positive values, as negative values will cull tiles near the camera
dynamicScreenSpaceErrorDensity >=0.0 The adjusted SSE falls off from the original value like a bell curve as distance from the camera increases, to simulate fog being more intense in the distance. Increasing this density parameter makes the bell curve have a sharper peak, which simulates thicker fog. Setting this value to 0.0 Has the same effect as turning off fog culling.
dynamicScreenSpaceErrorHeightFalloff [0.0, 1.0] (a percentage of the height range) Fog culling depends on the camera height such that the effect is largest when the camera is near "street level" and gradually falls off to no effect when the camera is above the data. This falloff value is a percentage of the height range of the data[^1]. When the camera is below the falloff point, fog culling will have the strongest effect. Between the falloff point and the maximum height, fog culling will gradually taper off. Above the maximum height, fog culling has no effect.

[^1]: The height range that is computed depends on the the bounding volume of the dataset. For smaller tilesets with a box/region bounding volume, this is based on the min/max height. For large world-scale bounding volumes, an approximate height range is computed relative to the ellipsoid.

Another observation: Fog culling rolls off with the camera angle. It has a maximum effect when the camera is facing the horizon, and no effect when the camera is facing straight up or straight down.

Here's a diagram of what the fog culling SSE adjustment looks like: image

Here's what the density parameter does: image

Here's what the SSE Factor parameter does: image

And here's an animation that shows how culling is strongest when the camera is low, but rolls off as the height increases past the falloff point. 2023-12-06_SSEFalloff2

ptrgags commented 9 months ago

Before we enable fog culling by default, we'd like to do some performance testing to evaluate how well it works and what parameters would be the best defaults.

For the first round of tests, I'm using Google Photorealistic 3D Tiles to test how well this feature helps reduce tiles loaded for world-scale datasets. This involves

  1. Measure initial tiles loaded time and tiles loaded/selected for the dataset without fog culling
  2. Measure it again with the default fog culling settings
  3. Try increasing just the density parameter to have the maximum effect (I'm going to try setting it rather high at 0.1)
  4. Try increasing just the SSE factor parameter to simulate somewhat aggressive fog without going crazy. I'm going to try 32 px. This is twice the default maximumSSE threshold, and causes a noticeable difference in the background of landscape views.

I've already started some of these measurements, I will post the data and a summary once I have the rest of the numbers.

ptrgags commented 9 months ago

P3DT Fog Performance Test Results

I ran the above tests for comparison. I used 5 different areas of Google P3DT, and for each area 3 views (aerial, street level, top-down) for comparison.

Here's an example HTML file for one of the test setups. I adjusted the parameters in the tilesetOptions object and then refreshing the page as needed). performance-example-setup.html.txt

Here's a summary of the time to first load, averaged over all the different views:

Configuration Average time to first load (s) % difference from no fog
No Fog 7.989 N/A
Fog enabled with defaults 5.678 -28.93%
Fog with SSE Factor = 32 3.658 -54.22%
Fog with density = 0.1 5.403 -32.37%

For the full data (which includes other metrics like number of tiles loaded), see this CSV file: Dynamic SSE Perf Testing - P3DT.csv

ptrgags commented 9 months ago

Here's some visual comparisons of the data in CesiumJS vs Google Earth to compare how much is culled. It's hard to get the camera views exactly the same since the camera controls are so different, but here's something roughly similar:

CesiumJS P3DT Google Earth Comments
image image CesiumJS seems to load more tiles in the distance.
image image Here Google Earth has a bit more detail in the distance
image image Google Earth has a bit more detail in the distance

So overall, it seems like dynamicScreenSpaceErrorFactor: 32 does create somewhat intense fog culling, but not extremely so.

ptrgags commented 9 months ago

Another thing we wanted to check: Does fog culling serve as a workaround for horizon culling for world-scale tilesets? Freeze frame will help here:

With fog off: image

With fog on, (SSEFactor 32): image

As I suspected, it doesn't completely replace horizon culling (as tiles on the other side of the globe still render), but it does reduce the LODs loaded. Not perfect, but definitely an improvement!

ggetz commented 9 months ago

Thanks @ptrgags for the research!

1) There does appear to be a significant performance gain from implementing fog, so we'll likely move forwards with the process 2) Based on the screenshot comparisons, we may want to do a bit of tweaking to the default parameters to get the best balance between performance and visual fidelity, but we're definitely in the right ballpark 3) It sounds like horizon culling, or even outright culling a tiling which is completely in fog, may provide and additional benefit, so we may want to take a look at that next after this is completed

ptrgags commented 9 months ago

I started prototyping a very barebones FogPipelineStage to add some code to the model shader to see how the czm_fog() function works. I was happily surprised that it (+ AutomaticUniforms + Fog) automatically handles applying fog and even the camera tilt.

Here's Google P3DT with czm_fog() applied (with a hard-coded false color for now) compared to a similar view of San Francisco with Cesium World Terrain. The comparison here is how much of the model is in fog with default settings:

image image

There's still several more details to determine:

ptrgags commented 9 months ago

More notes from exploring the existing globe shader and a discussion with @lilleyse:

ptrgags commented 9 months ago

Update: I added some new automatic uniforms and created some new builtin functions for computing the scattering color. I also created a new Atmosphere object (stored as scene.atmosphere) to handle updating the FrameState.

The shader for FogPipelineStage is still very bare-bones, there's quite a bit of code I have to add from GlobeFS. Also the color doesn't yet look correct, it's quite dark when screenshots above are lighter and a bit more bluish in comparison. But the fact that you see something and not a shader compilation error is progress.

image

There's still a lot left to do here, so let me organize my thoughts:

ptrgags commented 9 months ago

There were a couple typos so a couple uniforms were NaN, fixed them. Now the coloring looks better, but now I get this artifact where everything above the horizon looks darker:

image

I'll have to investigate further.

ptrgags commented 9 months ago

It looks like the scattering code is likely where it happens, though I'm still trying to figure out why.

Along the way, I'm finding some corner cases with some of the variables that seem a bit off to me, though these don't seem to be a problem for terrain. Documenting them for future reference.

First, the variable w_stop_gt_lprl (weight for representing whether [ray intersection].stop greater than length(primaryRayLength)). This is a measure of "sky or horizon?" It seems to be sensitive to the difference between an ellipsoid and a sphere since it uses a sphere for the intersection test.. It seems to hover around 0.5 due to the 0.5 + 0.5 tanh(x) almost everywhere, but when the camera zooms in near the poles it jumps to 1.0 due to the camera being above the ellipsoid but underneath the sphere used for the test.

Second, the variable w_inside_atmosphere has a weird behavior on the horizon but only for certain camera views. it's 1 wherever it is yellow. The yellow at the bottom seems consistent with the behavior in most views when the camera is hovering close above the earth, but not sure why it's also 1 on the horizon:

image

Still poking around the code a bit. I also want to do a bit of reading up on atmosphere rendering to understand the scattering functions better.

ptrgags commented 9 months ago

This article was a helpful resource at understanding the concepts here about Rayleigh and Mie scattering, with plenty of good diagrams.

ptrgags commented 9 months ago

Well that's one thing that seems off. For fragments above the horizon, the lightHeight computed in the inner loop seems to be negative somehow :thinking:

image

lightHeight = length(lightPosition) - innerAtmosphereRadius so either the reference point (which is based on positionWC for the fragment) is wrong, or the light position calculation is wrong for fragments above the horizon.

ptrgags commented 9 months ago

Huh, when rendering (innerAtmosphereRadius / 1e7, length(lightPosition) / 1e7), both variables seem to get noticeably larger above the horizon. not sure why.

image

ptrgags commented 9 months ago

Hm, rendering positionWC = computeEllipsoidPosition() (same exact function as in GlobeFS.glsl)

I get weird values. the reddish colors above the horizon would mean +x here which is definitely wrong (+x is eastern hemisphere, this view is California). Also not sure why there are two colors.

image

Terrain for comparison for a similar view. All blue, even above the horizon.

image

So something about the coordinates used for the computation must be different between globe and Model... though this function uses only built-in uniforms so not sure what's different here.

vec3 czm_computeEllipsoidPosition()
{
    float mpp = czm_metersPerPixel(vec4(0.0, 0.0, -czm_currentFrustum.x, 1.0), 1.0);
    vec2 xy = gl_FragCoord.xy / czm_viewport.zw * 2.0 - vec2(1.0);
    xy *= czm_viewport.zw * mpp * 0.5;

    vec3 direction = normalize(vec3(xy, -czm_currentFrustum.x));
    czm_ray ray = czm_ray(vec3(0.0), direction);

    vec3 ellipsoid_center = czm_view[3].xyz;

    czm_raySegment intersection = czm_rayEllipsoidIntersectionInterval(ray, ellipsoid_center, czm_ellipsoidInverseRadii);

    vec3 ellipsoidPosition = czm_pointAlongRay(ray, intersection.start);
    return (czm_inverseView * vec4(ellipsoidPosition, 1.0)).xyz;
}
ptrgags commented 9 months ago

In SpectorJS, the only difference in uniforms is the frustum is quite a bit different. Model on the left, terrain on right:

image

ptrgags commented 9 months ago

Today I've been focusing on other details of 3D Tiles fog and thinking towards the future of atmosphere rendering.

See https://github.com/CesiumGS/cesium/issues/11681#issuecomment-1865080064 for more details on what I think the atmosphere setting structure should be (although getting there requires quite a few deprecations). For now, I'm sticking to non-breaking changes until it can be discussed further.

Then I wired up the flags that enable/disable the fog pipeline stage. fog.enabled/fog.renderable are the main settings that control this. This is simpler than for terrain which is tightly coupled to whether the globe exists. 2023-12-20_EnablingFog

ptrgags commented 9 months ago

I added an enum to select between the 3 dynamic lighting modes (OFF, SCENE_LIGHT, SUN_LIGHT like I described in the other issue (as this is cleaner than 2 separate flags that depend on each other). Here's it in action:

2023-12-20_DynamicLighting

For safekeeping, here's a Local Sandcastle for WIP branch 4196-3d-tiles-fog

ptrgags commented 9 months ago

@ggetz I clarified the product details of 3D Tiles fog with @shehzan10 today, and he clarified that right now the priority is focusing on 3D Tiles performance for world-scale datasets like Google P3DT. The visual rendering details are at a lower priority for now. So I'm splitting this issue up into the following smaller issues:

I'm closing this issue in favor of the 4 above.