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

Big performance drop from v1.93 onward #10510

Closed cmcleese closed 1 year ago

cmcleese commented 2 years ago

Sandcastle example: CodeSandbox Cesium v1.92 CodeSandbox Cesium v.193

Browser: Chrome 103.0.5060.66

Operating System: Win10

Important: Open codeSanbox link and select the open in new window button in the top of the preview window bar.
Performance will not be noticeable unless open in new window.

Both cesium home locations in the sandboxes are set to the same location and camera view.
All version from v1.93 and up seem to have the same performance drop. Tested with 1.93, 1.94, 1.95 with same issue. What used to be average 60fps is now 30fps.

image image

cmcleese commented 2 years ago

My initial observations is pointing to with the changes/additions in v1.93 of Improved rendering of ground and sky atmosphere. https://github.com/CesiumGS/cesium/pull/10063

sanjeetsuhag commented 2 years ago

Hi @cmcleese

Could you share information (https://webglreport.com/) about the system that you are seeing this issue on?

cmcleese commented 2 years ago

https://webglreport.com/ results:

image image

sanjeetsuhag commented 2 years ago

I just tested on a device with a similar GPU and there is a performance hit: 54 FPS to 46 FPS average on a 1080p globe

image

sanjeetsuhag commented 2 years ago

There is more computation involved in the new atmosphere shader since it's a more physically accurate version. One option may be to expose the ray sampling numbers in the public API, allowing users to reduce the number, if needed, to help performance.

lilleyse commented 2 years ago

@sanjeetsuhag is the new atmosphere still evaluated per-vertex by default?

sanjeetsuhag commented 2 years ago

@lilleyse It's still the same conditions as before:

cmcleese commented 1 year ago

Is there any push to improve the performance impact due to this new implementation of atmosphere?

glathoud commented 1 year ago

Also interested in this, because of a use case flying close to the ground with a highly detailed elevation model. In that use case the performance degradation from 1.92 to 1.93 is noticeable: averaged over the flight, down from about 57 FPS to about 33 FPS, then after optimizing a few parameters got 37 FPS, still to low to update Cesium.

Note: Turning off skyBox and skyAtmosphere improves the FPS rate but still not as before.

glathoud commented 1 year ago

Experimented a bit. Further below I give a possible solution for performance improvement.

In AtmosphereCommon.glsl I tried to lower the original constants (PRIMARY_STEPS:16,LIGHT_STEPS:4) down to (4,2) . This sure improved the performance, but when flying through the atmosphere, a yellow glow appeared in the sky near the horizon.

I thought that the yellow glow came from a too big rayStepLength so I tried to limit its value along those lines: float rayStepLength = (primaryRayAtmosphereIntersect.stop - primaryRayAtmosphereIntersect.start) / float(max(10.0,PRIMARY_STEPS));

This solved the "yellow-glow-in-the-sky" issue, but distant landscape turned too dark.

So I built a two-case implementation, with the difference "sky vs. horizon" in mind, then later replaced the binary branch with a (1+tanh)/2-like weight for performance reasons.

In the "horizon" case, the constant rayStepLength value is replaced with a progressively increasing rayStepLength value. This ensures to be sampling close enough to the viewer and far enough, even if PRIMARY_STEPS has a small value (e.g. 4). This is what seems to fix the "yellow glow" issue.

The resulting modified AtmosphereCommon.glsl looks like this below. Modified: the two constants PRIMARY_STEPS and LIGHT_STEPS and the function computeScattering(). I hope this can help in some way to solve the issue.

const float ATMOSPHERE_THICKNESS = 111e3; // The thickness of the atmosphere in meters.

// --- Modified: constants
const int PRIMARY_STEPS = 4; // Number of times the ray from the camera to the world position (primary ray) is sampled.
const int LIGHT_STEPS = 2; // Number of times the light is sampled from the light source's intersection with the atmosphere to a sample position on the primary ray.

/**
 * This function computes the colors contributed by Rayliegh and Mie scattering on a given ray, as well as
 * the transmittance value for the ray.
 *
 * @param {czm_ray} primaryRay The ray from the camera to the position.
 * @param {float} primaryRayLength The length of the primary ray.
 * @param {vec3} lightDirection The direction of the light to calculate the scattering from.
 * @param {vec3} rayleighColor The variable the Rayleigh scattering will be written to.
 * @param {vec3} mieColor The variable the Mie scattering will be written to.
 * @param {float} opacity The variable the transmittance will be written to.
 * @glslFunction
 */
void computeScattering(
    czm_ray primaryRay,
    float primaryRayLength,
    vec3 lightDirection,
    float atmosphereInnerRadius,
    out vec3 rayleighColor,
    out vec3 mieColor,
    out float opacity
) {

    // Initialize the default scattering amounts to 0.
    rayleighColor = vec3(0.0);
    mieColor = vec3(0.0);
    opacity = 0.0;

    float atmosphereOuterRadius = atmosphereInnerRadius + ATMOSPHERE_THICKNESS;

    vec3 origin = vec3(0.0);

    // Calculate intersection from the camera to the outer ring of the atmosphere.
    czm_raySegment primaryRayAtmosphereIntersect = czm_raySphereIntersectionInterval(primaryRay, origin, atmosphereOuterRadius);

    // Return empty colors if no intersection with the atmosphere geometry.
    if (primaryRayAtmosphereIntersect == czm_emptyRaySegment) {
        return;
    }

    // --- Modified: soft choice between sky and landscape
    float lprl = length(primaryRayLength);
    float x = 1e-7 * primaryRayAtmosphereIntersect.stop / lprl;
    // w_stop_gt_lprl: similar to (1+tanh)/2
    // Value close to 0.0: close to the horizon
    // Value close to 1.0: above in the sky
    float w_stop_gt_lprl = max(0.0,min(1.0,(1.0 + x * ( 27.0 + x * x ) / ( 27.0 + 9.0 * x * x ))/2.0));

    // The ray should start from the first intersection with the outer atmopshere, or from the camera position, if it is inside the atmosphere.
    primaryRayAtmosphereIntersect.start = max(primaryRayAtmosphereIntersect.start, 0.0);
    // The ray should end at the exit from the atmosphere or at the distance to the vertex, whichever is smaller.
    primaryRayAtmosphereIntersect.stop = min(primaryRayAtmosphereIntersect.stop, lprl);

    // --- Modified: constant step in one case, increasing step in the other case
    float rayStepLengthIncrease = (1.0 - w_stop_gt_lprl)*(primaryRayAtmosphereIntersect.stop - primaryRayAtmosphereIntersect.start) / (float(PRIMARY_STEPS*(PRIMARY_STEPS+1))/2.0);
    float rayStepLength         = w_stop_gt_lprl * (primaryRayAtmosphereIntersect.stop - primaryRayAtmosphereIntersect.start) / max(7.0,float(PRIMARY_STEPS));

    // Setup for sampling positions along the ray - starting from the intersection with the outer ring of the atmosphere.
    float rayStepLength = (primaryRayAtmosphereIntersect.stop - primaryRayAtmosphereIntersect.start) / float(PRIMARY_STEPS);
    float rayPositionLength = primaryRayAtmosphereIntersect.start;

    vec3 rayleighAccumulation = vec3(0.0);
    vec3 mieAccumulation = vec3(0.0);
    vec2 opticalDepth = vec2(0.0);
    vec2 heightScale = vec2(u_atmosphereRayleighScaleHeight, u_atmosphereMieScaleHeight);

    // Sample positions on the primary ray.

    // --- Modified: possible small optimization
    vec3 samplePosition = primaryRay.origin + primaryRay.direction * (rayPositionLength);
    vec3 primaryPositionDelta = primaryRay.direction * rayStepLength;

    for (int i = 0; i < PRIMARY_STEPS; i++) {
        // Calculate sample position along viewpoint ray.
      samplePosition += primaryPositionDelta; // --- Modified: possible small optimization

        // Calculate height of sample position above ellipsoid.
        float sampleHeight = length(samplePosition) - atmosphereInnerRadius;

        // Calculate and accumulate density of particles at the sample position.
        vec2 sampleDensity = exp(-sampleHeight / heightScale) * rayStepLength;
        opticalDepth += sampleDensity;

        // Generate ray from the sample position segment to the light source, up to the outer ring of the atmosphere.
        czm_ray lightRay = czm_ray(samplePosition, lightDirection);
        czm_raySegment lightRayAtmosphereIntersect = czm_raySphereIntersectionInterval(lightRay, origin, atmosphereOuterRadius);

        float lightStepLength = lightRayAtmosphereIntersect.stop / float(LIGHT_STEPS);
        float lightPositionLength = 0.0;

        vec2 lightOpticalDepth = vec2(0.0);

        // Sample positions along the light ray, to accumulate incidence of light on the latest sample segment.
        // --- Modified: possible small optimization
        vec3 lightPosition = samplePosition + lightDirection * (lightStepLength * 0.5);
        vec3 lightPositionDelta = lightDirection * lightStepLength;
        for (int j = 0; j < LIGHT_STEPS; j++) {

            // Calculate sample position along light ray.
          lightPosition += lightPositionDelta; // --- Modified: possible small optimization

            // Calculate height of the light sample position above ellipsoid.
            float lightHeight = length(lightPosition) - atmosphereInnerRadius;

            // Calculate density of photons at the light sample position.
            lightOpticalDepth += exp(-lightHeight / heightScale) * lightStepLength;

            // Increment distance on light ray.
            lightPositionLength += lightStepLength;
        }

        // Compute attenuation via the primary ray and the light ray.
        vec3 attenuation = exp(-((u_atmosphereMieCoefficient * (opticalDepth.y + lightOpticalDepth.y)) + (u_atmosphereRayleighCoefficient * (opticalDepth.x + lightOpticalDepth.x))));

        // Accumulate the scattering.
        rayleighAccumulation += sampleDensity.x * attenuation;
        mieAccumulation += sampleDensity.y * attenuation;

        // Increment distance on primary ray.

        // --- Modified: increasing step length
        rayPositionLength += (rayStepLength+=rayStepLengthIncrease);
    }

    // Compute the scattering amount.
    rayleighColor = u_atmosphereRayleighCoefficient * rayleighAccumulation;
    mieColor = u_atmosphereMieCoefficient * mieAccumulation;

    // Compute the transmittance i.e. how much light is passing through the atmosphere.
    opacity = length(exp(-((u_atmosphereMieCoefficient * opticalDepth.y) + (u_atmosphereRayleighCoefficient * opticalDepth.x))));
}
ggetz commented 1 year ago

Thanks for investigating @glathoud! Do you have screenshot comparisons of the results?

If it's not too noticeable of a visual change, we may be able to go with your suggestion here. Another option could be to expose the number of steps through the public API for those who want a trade off between visual quality and performance, though that is non-ideal, as we want the default to be best for as many use cases as possible.

glathoud commented 1 year ago

Thanks for investigating @glathoud! Do you have screenshot comparisons of the results?

On my use case, yes, see below screenshots [1] [2].

If it's not too noticeable of a visual change, we may be able to go with your suggestion here.

Ok. I can prepare a pull request if needed/relevant.

Another option could be to expose the number of steps through the public API for those who want a trade off between visual quality and performance, though that is non-ideal, as we want the default to be best for as many use cases as possible.

Yes. However, only exposing the number of steps might lead to an unsatisfactory yellow glow, see screenshots [3][4][5]. That was the initial issue I encountered.

Alternatively a boolean switch between two implementations. This would also give some more space for future improvements without requiring the user to change anything.

.

Screenshot [1] Original 1.102 implementation

2023-02-13-172254_1920x1048_scrot

.

Screenshot [2] Modified 1.102 implementation (please ignore the labels). You may notice a slight difference in the sky - but maybe some additional work can fix this.

2023-02-21-122313_1920x1048_scrot

.

Screenshots [3][4] Initial issue: "Yellow glow at the horizon" when just lowering the *_STEPS numbers in the original implementation, down to (4,2). The yellow glow is a bit visible in the left part of [3], and a lot more visible in [4] where mountains are not blocking the horizon

[3]

2023-02-22-084315_1920x1048_scrot

[4]

2023-02-22-085350_1920x1048_scrot

.

[5] For reference, with the original implementation, original *_STEPS numbers (16,4), no yellow glow:

2023-02-22-084626_1920x1048_scrot

glathoud commented 1 year ago

Basically in [3][4] the sky looks like early morning before sunrise.

glathoud commented 1 year ago

I fixed the explanation of what I did, fixed comments in the code, but the actual code remains unchanged.

ggetz commented 1 year ago

Thanks @glathoud, screenshot 2 with your modified implementation looks promising! Would you possibly be able to open a PR with the changes and we can do a more in-depth review?

glathoud commented 1 year ago

@ggetz Sure, looking at the PR.

glathoud commented 1 year ago

@ggetz Ok the PR is ready: https://github.com/CesiumGS/cesium/pull/11109

ggetz commented 1 year ago

Closing after work in https://github.com/CesiumGS/cesium/pull/11109.