cesium
cesium copied to clipboard
Big performance drop from v1.93 onward
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.
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
Hi @cmcleese
Could you share information (https://webglreport.com/) about the system that you are seeing this issue on?
https://webglreport.com/ results:
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
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.
@sanjeetsuhag is the new atmosphere still evaluated per-vertex by default?
@lilleyse It's still the same conditions as before:
- for the sky atmosphere, unless set in the public API, sky is per-vertex
- for the ground atmosphere, when far way, atmosphere is per-fragment
Is there any push to improve the performance impact due to this new implementation of atmosphere?
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.
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))));
}
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.
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
.
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.
.
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]
[4]
.
[5] For reference, with the original implementation, original *_STEPS
numbers (16,4), no yellow glow:
Basically in [3][4] the sky looks like early morning before sunrise.
I fixed the explanation of what I did, fixed comments in the code, but the actual code remains unchanged.
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?
@ggetz Sure, looking at the PR.
@ggetz Ok the PR is ready: https://github.com/CesiumGS/cesium/pull/11109
Closing after work in https://github.com/CesiumGS/cesium/pull/11109.