Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix billboards when clamped to ground with depthTestAgainstTerrain #6621

Merged
merged 18 commits into from
Jun 21, 2018

Conversation

hpinkos
Copy link
Contributor

@hpinkos hpinkos commented May 23, 2018

Fixes #2694

Before:
image

After:
image

  • This only changes billboards using HeightReference.CLAMP_TO_GROUND
  • When the camera is greater than 2000m away from the billboard, use normal depth test against terrain
    • 2000 was picked arbitrarily through testing to see what looked reasonable
  • When the camera is closer than 2000m to the billboard, check if the top left, top right and anchor points are behind terrain. If all 3 points are behind terrain, hide the billboard. Otherwise, show the billboard.
    • This can cause the billboard to show through terrain in some edge cases, but I think overall it's a vast improvement to what we have and brings us closer to enabling terrain by default.

Test code:

var viewer = new Cesium.Viewer('cesiumContainer', {
    terrainProvider: Cesium.createWorldTerrain()
});
viewer.scene.globe.depthTestAgainstTerrain = true;

var e = viewer.entities.add({
    position : Cesium.Cartesian3.fromDegrees(-122.1958, 46.1915),
    billboard : {
        image : '../images/Cesium_Logo_overlay.png',
        heightReference : Cesium.HeightReference.CLAMP_TO_GROUND,
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM
    }
});

viewer.trackedEntity = e;

@cesium-concierge
Copy link

Signed CLA is on file.

@hpinkos, thanks for the pull request! Maintainers, we have a signed CLA from @hpinkos, so you can review this at any time.

⚠️ I noticed that CHANGES.md has not been updated. If this change updates the public API in any way, fixes a bug, or makes any non-trivial update, please add a bullet point to CHANGES.md and comment on this pull request so we know it was updated. For more info, see the Pull Request Guidelines.


I am a bot who helps you make Cesium awesome! Contributions to my configuration are welcome.

🌍 🌎 🌏

@hpinkos
Copy link
Contributor Author

hpinkos commented May 23, 2018

Thanks for helping me with this @bagnell!

What's the best way to write tests for this kind of thing?

@bagnell
Copy link
Contributor

bagnell commented May 23, 2018

I haven't seen the code yet, but I imagine you need to mock the globe so the clamp to ground just returns some constant height value. Also, see the ground primitive tests
and classification tests. They change the primitive commands to execute during the globe pass which will be written into the globe depth texture.

@hpinkos
Copy link
Contributor Author

hpinkos commented May 25, 2018

@bagnell I tried to write some tests, but everything breaks down trying to do a render test with height referenced billboards because it relies on the QuadtreePrimitive for adjusting the heights. I'm guessing we don't have a good way to mock that and that's why we don't have any render tests for billboards using a height reference.

I think this is ready for review. If you had other ideas for how to add tests let me know

#ifdef CLAMP_TO_GROUND
if (v_eyeDepth > -2000.0) {
vec2 adjustedST = v_textureCoordinates - v_textureOffset.xy;
adjustedST = adjustedST / (v_textureOffset.z - v_textureOffset.x, v_textureOffset.w - v_textureOffset.y);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised this compiled. Add use the vec2 constructor for the denominator.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks haha. I hardly ever glsl so I'm not very familiar with the syntax

vec2 st1 = ((v_dimensions.xy * (v_depthLookupTextureCoordinate1 - adjustedST)) + gl_FragCoord.xy) / czm_viewport.zw;
float logDepthOrDepth = czm_unpackDepth(texture2D(czm_globeDepthTexture, st1));
vec4 eyeCoordinate = czm_windowToEyeCoordinates(gl_FragCoord.xy, logDepthOrDepth);
float globeDepth1 = eyeCoordinate.z / eyeCoordinate.w;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this and the above two lines a function.

eyeCoordinate = czm_windowToEyeCoordinates(gl_FragCoord.xy, logDepthOrDepth);
float globeDepth2 = eyeCoordinate.z / eyeCoordinate.w;

if (globeDepth2 > v_eyeDepth + czm_epsilon5)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One would think that all of these ifs would increase performance for fragments that don't execute them, but, unfortunately, that's not the case. It would look cleaner if you compute all of the globe depths and then have one if combining all four conditionals for the discard.

@@ -152,6 +166,14 @@ void main()

vec2 imageSize = vec2(floor(temp), compressedAttribute2.w);

#ifdef CLAMP_TO_GROUND
v_textureOffset = textureOffset;
v_depthLookupTextureCoordinate1 = (0.0, 0.9) - depthLookupST; //the origin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure these are right? Say the anchor point is the bottom right, then wouldn't you want to test vec2(-1.0, 1.0) instead of vec2(1.0)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the vec* constructors, a single argument duplicates it for all components.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right. I think I forgot to test different horizontal origins, this only works for center.

@hpinkos
Copy link
Contributor Author

hpinkos commented Jun 5, 2018

@bagnell ready for another look.
Any ideas for what we can do for labels? I guess each letter is a separate billboard, which doesn't work well with this approach.

label-ground

var viewer = new Cesium.Viewer('cesiumContainer', {
    terrainProvider: Cesium.createWorldTerrain()
});
viewer.scene.globe.depthTestAgainstTerrain = true;

var e = viewer.entities.add({
    position : Cesium.Cartesian3.fromDegrees(-122.1958, 46.1915),
     label : {
        text : 'Clamped to ground',
        heightReference : Cesium.HeightReference.CLAMP_TO_GROUND,
        horizontalOrigin : Cesium.HorizontalOrigin.CENTER,
        verticalOrigin : Cesium.VerticalOrigin.BOTTOM,
        fillColor : Cesium.Color.BLACK,
        showBackground : true,
        backgroundColor : new Cesium.Color(1, 1, 1, 0.7),
        backgroundPadding : new Cesium.Cartesian2(8, 4)
    }
});

viewer.trackedEntity = e;

@bagnell
Copy link
Contributor

bagnell commented Jun 5, 2018

Look for translate in the vertex shader. It is the pixel offset to move each billboard in a label.

Did you test this with all of the other billboard options? You'll have to account for pixelOffset, sizeInMeters, *ByDistance, and others.

@hpinkos
Copy link
Contributor Author

hpinkos commented Jun 6, 2018

@bagnell as far as I can tell, this is working correctly with things like scale, pixelOffset *ByDistance and sizeInMeters.

The problem with the label is that each letter is checking the bounds of that letter instead of checking the bounds of the entire label. That's why each letter flickers individually.

@bagnell
Copy link
Contributor

bagnell commented Jun 6, 2018

Can you add another attribute for the size of the whole label?

pjcozzi
pjcozzi previously requested changes Jun 9, 2018
Copy link
Contributor

@pjcozzi pjcozzi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hpinkos do you want to write a short tech blog post about this?

@@ -302,7 +311,9 @@ define([
BufferUsage.STATIC_DRAW, // SCALE_BY_DISTANCE_INDEX
BufferUsage.STATIC_DRAW, // TRANSLUCENCY_BY_DISTANCE_INDEX
BufferUsage.STATIC_DRAW, // PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX
BufferUsage.STATIC_DRAW // DISTANCE_DISPLAY_CONDITION_INDEX
BufferUsage.STATIC_DRAW, // DISTANCE_DISPLAY_CONDITION_INDEX
BufferUsage.STATIC_DRAW, // TEXTURE_OFFSET
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra whitespace?

usage : buffersUsage[TEXTURE_OFFSET]
}, {
index : attributeLocations.dimensions,
componentsPerAttribute : 2,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anyway to pack this into one component and perhaps store as z in DISTANCE_DISPLAY_CONDITION_INDEX? Haven't looked closely, but perhaps precision requirements are not stiff here and fewer bits can be used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to pack a Cartesian2 into one float? I'm not quite sure how to do this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no bit-wise operators in GLSL but you can shift bits by multiplying powers of 2. See any of the writeCompressedAttrib* functions. So if you want to pack a Cartesian2 into a float, you'll have 12 bits for each component. Is that enough precision? It would look something like this for packing:

var LEFT_SHIFT12 = Math.pow(2.0, 12.0);
var width = Math.floor(Math.clamp(width, 0.0, LEFT_SHIFT12));
var height = Math.floor(Math.clamp(height, 0.0, LEFT_SHIFT12));
var dimensions = width * LEFT_SHIFT12 + height;

Then, reverse it in the vertex shader to unpack.

@@ -1176,6 +1197,10 @@ define([
}

var disableDepthTestDistance = billboard.disableDepthTestDistance;
if (billboard.heightReference === HeightReference.CLAMP_TO_GROUND && disableDepthTestDistance === 0.0) {
disableDepthTestDistance = 2000.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be configurable at the scene level? I think there are other billboard/label/depth-ish options there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's configurable by setting disableDepthTestDistance to a different value

#ifdef VECTOR_TILE
attribute float a_batchId;
#endif

varying vec2 v_textureCoordinates;
#ifdef CLAMP_TO_GROUND
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to use a huge amount of memory bandwidth.

Looks easy to pack some varyings together. Also perhaps some can be derived from others or more should be computed in the fragment shader if they are trivial?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, use more precise names - if possible - than 1, 2, and 3 for texture coordinates.

@@ -10,11 +10,25 @@ attribute vec4 eyeOffset; // eye offset in mete
attribute vec4 scaleByDistance; // near, nearScale, far, farScale
attribute vec4 pixelOffsetScaleByDistance; // near, nearScale, far, farScale
attribute vec3 distanceDisplayConditionAndDisableDepth; // near, far, disableDepthTestDistance
#ifdef CLAMP_TO_GROUND
attribute vec4 textureOffset; // the min and max x and y values for the texture coordinates
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be named something more precise like textureCoordinatesBounds?

v_depthLookupTextureCoordinate1 = vec2(1.0) - depthOrigin; //the origin
if (v_depthLookupTextureCoordinate1.y == 1.0) //vertical origin is top
{
v_depthLookupTextureCoordinate2 = vec2(0.0, 0.0); //bottom left
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just vec2(0.0)

float epsilonEyeDepth = v_eyeDepth + czm_epsilon5;

// negative values go into the screen
if (globeDepth1 > epsilonEyeDepth && globeDepth2 > epsilonEyeDepth && globeDepth3 > epsilonEyeDepth)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a separate if statement after each call to getGlobeDepth.

On WebGL implementations with dynamic branches, it could exit early and avoid the memory bandwidth for the unnecessary texture reads.

Also, if not already done, order the calls to getGlobeDepth in the order of most likely to fail the depth test - I imagine origin first.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had it that way initially but @bagnell said it wouldn't work that way. Is dynamic branches something that's only supported in certain browsers?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a lot of GPUs with WebGL 1 support, there is no dynamic branches, but @pjcozzi is right. Early exits for GPUs with dynamic branches would be faster.

@pjcozzi
Copy link
Contributor

pjcozzi commented Jun 9, 2018

CHANGES.md.

@hpinkos
Copy link
Contributor Author

hpinkos commented Jun 12, 2018

@bagnell this is ready for another review

@bagnell
Copy link
Contributor

bagnell commented Jun 13, 2018

You can get the background to disappear but not the glyphs. I used the code you posted above but changes the horizontal origin to be LEFT and the text is just 'Clamped'.
image

{
if (v_originTextureCoordinateAndTranslate.y == 0.0)
{
v_originTextureCoordinateAndTranslate.y = 0.1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 0.1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It had better results than testing the bottom edge when I was first playing around with this, but I'll revisit it because I'm not sure if that's still the case

czm_writeLogDepth();

#ifdef CLAMP_TO_GROUND
if (v_eyeDepthAndDistance.x > -v_eyeDepthAndDistance.y) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much of a pain would it be to add this to both the vertex and fragment shader?

We would want it to execute in the vertex shader when ContextLimits.maximumVertexTextureImageUnits > 0; otherwise, execute in the fragment shader. That condition just means we can read from textures in the vertex shader. The reason we would want to do this is for performance. Instead of discarding the fragment, you would put the vertex behind the eye so that it gets clipped like we do for distance display conditions or translucency by distance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to do this in a separate PR because the math is different since there's no gl_FragCoord in the vertex shader. I'll open an issue

@bagnell
Copy link
Contributor

bagnell commented Jun 13, 2018

The code looks good to me. The biggest thing would be executing that code in the vertex shader when possible.

Does anyone else want to test?

@hpinkos
Copy link
Contributor Author

hpinkos commented Jun 19, 2018

@bagnell I think I've finally worked out all the kinks. Labels were actually looking up pixels in the completely wrong location because the horizontal origin passed in is always "LEFT", so I had to pass in the horizontal offset the user set separately.

There is still occasionally a 1px discrepancy that can lead to this:

image

I'm honestly not sure how to fix it. I think it's a rounding error. The texture lookup is literally 1px different and it doesn't happen all of the time.

@bagnell
Copy link
Contributor

bagnell commented Jun 19, 2018

This crashes in IE since it doesn't have depth texture support.

@hpinkos
Copy link
Contributor Author

hpinkos commented Jun 19, 2018

@bagnell thanks for catching that. I changed IE so it has the behavior currently in master. I also tested Firefox and Edge and they both work great. And all the tests pass locally.

Anything else?

@bagnell
Copy link
Contributor

bagnell commented Jun 19, 2018

I still see the missing glyphs when the origin is left center. I also see all the glyphs disappear but the background is still visible when the origin is right center.

@bagnell
Copy link
Contributor

bagnell commented Jun 19, 2018

This is with the scale set to 1.5:
label_depth_test_scale
EDIT: I'm seeing the same issue with sizeInMeters : true.

@bagnell
Copy link
Contributor

bagnell commented Jun 19, 2018

I'm seeing some strange behavior with rotated billboards. This has an origin at the bottom center and a rotation of pi/4.
billboard_depth_test_rotation
EDIT: Same issue with aligned axis.

@hpinkos
Copy link
Contributor Author

hpinkos commented Jun 19, 2018

Dagnabbit. I thought I checked all the weird properties but I guess I missed some stuff 😓

@hpinkos
Copy link
Contributor Author

hpinkos commented Jun 20, 2018

@bagnell I fixed scale and rotation/alignedAxis. I'm a little stuck on sizeInMeters though

@bagnell
Copy link
Contributor

bagnell commented Jun 20, 2018

To fix sizeInMeters, multiply anything that has units in pixels with czm_metersPerPixel(eyeCoordinates).

@hpinkos
Copy link
Contributor Author

hpinkos commented Jun 21, 2018

@bagnell This should be ready for another look
I do still occasionally see blinking letters because of what I mentioned in this comment: #6621 (comment)
The PR you submitted made it better, but it didn't fix it completely. I'm going to play around with adding floor to different pixel calculations to see if that fixes it. Right now the way we compute translate has all kinds of fractional values and I think things like that are causing the lookup to be 1px off every now and then.

But I believe I've fixed everything that was really wrong. I tested a combination of different origins, scale, rotation and it seems to have pretty good behavior.

@mramato
Copy link
Contributor

mramato commented Jun 21, 2018

I'm going to play around with adding floor to different pixel calculations to see if that fixes it.

Be careful with this, clamping pixels can lead to very undesired behavior (especially when the text is moving). That's the main reason we stopped clamping to screen space. Not saying it applies here, just something to be aware of.

@hpinkos
Copy link
Contributor Author

hpinkos commented Jun 21, 2018

Be careful with this, clamping pixels can lead to very undesired behavior (especially when the text is moving)

@mramato thanks for noting that. I should be able to make the location of the depth lookup consistent without effecting the position of the billboards.

@hpinkos
Copy link
Contributor Author

hpinkos commented Jun 21, 2018

@bagnell nope, that didn't seem to help with the label letters. Do you have any ideas? I feel like it only happens if you're trying really hard and might not be a huge issue in normal application use. Regardless, the behavior here is way better than what's in master now.

@bagnell
Copy link
Contributor

bagnell commented Jun 21, 2018

This looks good to me. I wasn't able to reproduce the missing letters at all where the last time I commented I could reproduce it reliably.

I'm merging this but don't forget about #6699.

@bagnell bagnell merged commit 0a23971 into master Jun 21, 2018
@bagnell bagnell deleted the billboard-depth-test branch June 21, 2018 23:45
@esparano
Copy link

Is there a reason that this change only affects billboards with HeightReference.CLAMP_TO_GROUND? I'm having the same issues with billboards that are very close to (but not clamped to) the ground, so I had hoped that those would be fixed by this change.

On a side note, in my specific case, I was looking for a way to hide billboards based on ONLY the anchor point's visibility. Perhaps that could be an option in the future!

@pjcozzi
Copy link
Contributor

pjcozzi commented Jun 30, 2018

@esparano good point, this would also be useful for labels "on" 3D buildings, etc.

@hpinkos how hard is it to make this a separate boolean? If easy, maybe do this soon; otherwise, submit an issue. Should it default to true? Is there ever a case when it should be false...hmm, might not need a flag at all if everyone likes the new depth testing...I dunno.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Labels/billboards on terrain and depthTestAgainstTerrain
6 participants