Compare commits

..

6 Commits

Author SHA1 Message Date
Marco Hutter bdcd200d47
Merge 1a8764265f into 41a0f18ba9 2025-11-25 18:28:48 -05:00
Don McCurdy 41a0f18ba9
Merge pull request #13018 from CesiumGS/billboard-depth-regression-fix
Fixes model-billboard depth interactions
2025-11-25 18:03:41 +00:00
Don McCurdy 8bd8ae7f63
Merge branch 'main' into billboard-depth-regression-fix 2025-11-25 13:02:57 -05:00
Marco Hutter 1a8764265f Squashed commit with cleanups and additional tests
deploy / deploy (push) Has been cancelled Details
2025-11-25 18:24:35 +01:00
Matt Schwartz 020749de32 Add to CHANGES.md 2025-11-05 09:27:19 -05:00
Matt Schwartz f4e49d7b64 Fixes model-billboard depth interactions 2025-11-05 09:12:11 -05:00
5 changed files with 1725 additions and 742 deletions

View File

@ -1,11 +1,12 @@
# Change Log # Change Log
## 1.136 ## 1.136 - 2025-12-01
### @cesium/engine ### @cesium/engine
#### Fixes :wrench: #### Fixes :wrench:
- Fixed depth testing bug with billboards and labels clipping through models [#13012](https://github.com/CesiumGS/cesium/issues/13012)
- Billboards using `imageSubRegion` now render as expected. [#12585](https://github.com/CesiumGS/cesium/issues/12585) - Billboards using `imageSubRegion` now render as expected. [#12585](https://github.com/CesiumGS/cesium/issues/12585)
- Improved scaling of SVGs in billboards [#13020](https://github.com/CesiumGS/cesium/issues/13020) - Improved scaling of SVGs in billboards [#13020](https://github.com/CesiumGS/cesium/issues/13020)
- Fixed unexpected outline artifacts around billboards [#13038](https://github.com/CesiumGS/cesium/issues/13038) - Fixed unexpected outline artifacts around billboards [#13038](https://github.com/CesiumGS/cesium/issues/13038)

View File

@ -34,6 +34,7 @@ import SDFSettings from "./SDFSettings.js";
import TextureAtlas from "../Renderer/TextureAtlas.js"; import TextureAtlas from "../Renderer/TextureAtlas.js";
import VerticalOrigin from "./VerticalOrigin.js"; import VerticalOrigin from "./VerticalOrigin.js";
import Ellipsoid from "../Core/Ellipsoid.js"; import Ellipsoid from "../Core/Ellipsoid.js";
import WebGLConstants from "../Core/WebGLConstants.js";
const SHOW_INDEX = Billboard.SHOW_INDEX; const SHOW_INDEX = Billboard.SHOW_INDEX;
const POSITION_INDEX = Billboard.POSITION_INDEX; const POSITION_INDEX = Billboard.POSITION_INDEX;
@ -2039,7 +2040,8 @@ BillboardCollection.prototype.update = function (frameState) {
) { ) {
this._rsOpaque = RenderState.fromCache({ this._rsOpaque = RenderState.fromCache({
depthTest: { depthTest: {
enabled: false, enabled: true,
func: WebGLConstants.LESS,
}, },
depthMask: true, depthMask: true,
}); });
@ -2059,7 +2061,10 @@ BillboardCollection.prototype.update = function (frameState) {
) { ) {
this._rsTranslucent = RenderState.fromCache({ this._rsTranslucent = RenderState.fromCache({
depthTest: { depthTest: {
enabled: false, enabled: true,
func: useTranslucentDepthMask
? WebGLConstants.LEQUAL
: WebGLConstants.LESS,
}, },
depthMask: useTranslucentDepthMask, depthMask: useTranslucentDepthMask,
blending: BlendingState.ALPHA_BLEND, blending: BlendingState.ALPHA_BLEND,

View File

@ -48,8 +48,22 @@ class NDMap {
* this constructor. * this constructor.
* *
* @param {string[]} dimensionNames * @param {string[]} dimensionNames
* @throws {DeveloperError} If the given array has length 0 or
* contains duplicate elements
*/ */
constructor(dimensionNames) { constructor(dimensionNames) {
if (dimensionNames.length === 0) {
throw new DeveloperError(
"The dimensionNames array may not have length 0",
);
} else {
const s = new Set(dimensionNames);
if (s.size !== dimensionNames.length) {
throw new DeveloperError(
`The dimensionNames array may not contain duplicate elements, but is ${dimensionNames}`,
);
}
}
this._dimensionNames = dimensionNames; this._dimensionNames = dimensionNames;
/** /**
@ -75,7 +89,7 @@ class NDMap {
* @returns {number} The size * @returns {number} The size
*/ */
get size() { get size() {
return this._lookup.size(); return this._lookup.size;
} }
/** /**
@ -198,7 +212,7 @@ class NDMap {
* @param {any} thisArg A value to use as this when executing the callback * @param {any} thisArg A value to use as this when executing the callback
*/ */
forEach(callback, thisArg) { forEach(callback, thisArg) {
this._entries().forEach(callback, thisArg); this.entries().forEach(callback, thisArg);
} }
/** /**
@ -247,12 +261,18 @@ class LRUCache {
/** /**
* Creates a new instance with the given maximum size. * Creates a new instance with the given maximum size.
* *
* @param {number} maxSize The maximum size * @param {number} maximumSize The maximum size
* @param {Function|undefined} evictionCallback The callback that will * @param {Function|undefined} evictionCallback The callback that will
* receive the key and value of all evicted entries. * receive the key and value of all evicted entries.
* @throws {DeveloperError} If the maximum size is not positive
*/ */
constructor(maxSize, evictionCallback) { constructor(maximumSize, evictionCallback) {
this._maxSize = maxSize; if (maximumSize <= 0) {
throw new DeveloperError(
`The maximumSize must be positive, but is ${maximumSize}`,
);
}
this._maximumSize = maximumSize;
this._evictionCallback = evictionCallback; this._evictionCallback = evictionCallback;
/** /**
@ -271,11 +291,17 @@ class LRUCache {
* of this cache, then the least recently used elements will * of this cache, then the least recently used elements will
* be evicted until the size matches the maximum size. * be evicted until the size matches the maximum size.
* *
* @param {number} maxSize The maximum size * @param {number} maximumSize The maximum size
* @throws {DeveloperError} If the maximum size is not positive
*/ */
setMaximumSize(maxSize) { setMaximumSize(maximumSize) {
this._maxSize = maxSize; if (maximumSize <= 0) {
this._ensureMaxSize(); throw new DeveloperError(
`The maximumSize must be positive, but is ${maximumSize}`,
);
}
this._maximumSize = maximumSize;
this._ensureMaximumSize();
} }
/** /**
@ -296,7 +322,7 @@ class LRUCache {
set(key, value) { set(key, value) {
this._map.delete(key); this._map.delete(key);
this._map.set(key, value); this._map.set(key, value);
this._ensureMaxSize(); this._ensureMaximumSize();
} }
/** /**
@ -326,8 +352,8 @@ class LRUCache {
* This will evict as many entries as necessary, in the * This will evict as many entries as necessary, in the
* order of their least recent usage. * order of their least recent usage.
*/ */
_ensureMaxSize() { _ensureMaximumSize() {
this.trimToSize(this._maxSize); this.trimToSize(this._maximumSize);
} }
/** /**
@ -490,6 +516,18 @@ class LoggingRequestListener extends RequestListener {
} }
} }
// TODO If something like the RequestHandle and RequestListener
// had to be designed from scratch, then it could be possible
// to come up with something that makes sense. Now, there's the
// question about how to align the sought-for "nice" solution
// with what is already there. A specific example is that
// the "requestAttempted" function does not really make sense,
// but has to be there, because this was once tracked in some
// tileset statistics. (Otherwise, VERY roughly, there could
// be the invariant of "cancelled+failed==attempted", but
// "attempted" can also mean that nothing really happened
// due to throttling, soooo... here we are...)
/** /**
* A class serving as a convenience wrapper around a request for * A class serving as a convenience wrapper around a request for
* a resource. * a resource.
@ -686,7 +724,7 @@ class RequestHandle {
const request = new Request({ const request = new Request({
throttle: true, throttle: true,
throttleByServer: true, throttleByServer: true,
type: RequestType.TILES3D, type: RequestType.TILES3D, // XXX_DYNAMIC TODO Seems to be unused...
priorityFunction: priorityFunction, priorityFunction: priorityFunction,
}); });
return request; return request;
@ -708,6 +746,8 @@ class RequestHandle {
const rejectionError = new Error("Request was cancelled"); const rejectionError = new Error("Request was cancelled");
rejectionError.code = RequestState.CANCELLED; rejectionError.code = RequestState.CANCELLED;
this._deferred.reject(rejectionError); this._deferred.reject(rejectionError);
this._fireRequestCancelled();
this._fireRequestAttempted();
} }
/** /**
@ -1361,7 +1401,7 @@ class Dynamic3DTileContent {
* *
* @type {number} * @type {number}
*/ */
this._loadedContentHandlesMaxSize = 10; this._loadedContentHandlesMaximumSize = 10;
/** /**
* The mapping from "keys" to arrays(!) of URIs for the dynamic content. * The mapping from "keys" to arrays(!) of URIs for the dynamic content.
@ -1397,7 +1437,7 @@ class Dynamic3DTileContent {
* evicted from the '_loadedContentHandles'. * evicted from the '_loadedContentHandles'.
* *
* This will be called when the size of the '_loadedContentHandles' * This will be called when the size of the '_loadedContentHandles'
* is trimmed to the '_loadedContentHandlesMaxSize', and receive * is trimmed to the '_loadedContentHandlesMaximumSize', and receive
* the least recently used content handles. * the least recently used content handles.
* *
* It will call 'reset()' on the content handle, cancelling all * It will call 'reset()' on the content handle, cancelling all
@ -1998,15 +2038,15 @@ class Dynamic3DTileContent {
// Ensure that at least the number of active contents // Ensure that at least the number of active contents
// is retained // is retained
const numActiveContents = activeContentUris.length; const numActiveContents = activeContentUris.length;
this._loadedContentHandlesMaxSize = Math.max( this._loadedContentHandlesMaximumSize = Math.max(
this._loadedContentHandlesMaxSize, this._loadedContentHandlesMaximumSize,
numActiveContents, numActiveContents,
); );
// Trim the LRU cache to the target size, calling the // Trim the LRU cache to the target size, calling the
// '_loadedContentHandleEvicted' for the least recently // '_loadedContentHandleEvicted' for the least recently
// used content handles. // used content handles.
loadedContentHandles.trimToSize(this._loadedContentHandlesMaxSize); loadedContentHandles.trimToSize(this._loadedContentHandlesMaximumSize);
} }
/** /**

View File

@ -131,7 +131,9 @@ void doThreePointDepthTest(float eyeDepth, bool applyTranslate) {
} }
#endif #endif
void doDepthTest() { // Extra manual depth testing is done to allow more control over how a billboard is occluded
// by the globe when near and far from the camera.
void doDepthTest(float globeDepth) {
float temp = v_compressed.y; float temp = v_compressed.y;
temp = temp * SHIFT_RIGHT1; temp = temp * SHIFT_RIGHT1;
float temp2 = (temp - floor(temp)) * SHIFT_LEFT1; float temp2 = (temp - floor(temp)) * SHIFT_LEFT1;
@ -156,14 +158,10 @@ void doDepthTest() {
} }
#endif #endif
// Automatic depth testing of billboards is disabled (@see BillboardCollection#update).
// Instead, we do one of two types of manual depth tests (potentially in addition to the test above), depending on the camera's distance to the billboard fragment.
// If we're far away, we just compare against a flat, camera-facing depth-plane at the ellipsoid's center. // If we're far away, we just compare against a flat, camera-facing depth-plane at the ellipsoid's center.
// If we're close, we compare against the globe depth texture (which includes depth from the 3D tile pass). // If we're close, we compare against the globe depth texture (which includes depth from the 3D tile pass).
vec2 fragSt = gl_FragCoord.xy / czm_viewport.zw;
float globeDepth = getGlobeDepthAtCoords(fragSt); if (globeDepth == 0.0) return; // Not on globe
if (globeDepth == 0.0) return; // Not on globe
float distanceToEllipsoidCenter = -length(czm_viewerPositionWC); // depth is negative by convention float distanceToEllipsoidCenter = -length(czm_viewerPositionWC); // depth is negative by convention
float testDistance = (eyeDepth > -u_coarseDepthTestDistance) ? globeDepth : distanceToEllipsoidCenter; float testDistance = (eyeDepth > -u_coarseDepthTestDistance) ? globeDepth : distanceToEllipsoidCenter;
if (eyeDepth < testDistance) { if (eyeDepth < testDistance) {
@ -175,7 +173,10 @@ void main()
{ {
if (v_splitDirection < 0.0 && gl_FragCoord.x > czm_splitPosition) discard; if (v_splitDirection < 0.0 && gl_FragCoord.x > czm_splitPosition) discard;
if (v_splitDirection > 0.0 && gl_FragCoord.x < czm_splitPosition) discard; if (v_splitDirection > 0.0 && gl_FragCoord.x < czm_splitPosition) discard;
doDepthTest();
vec2 fragSt = gl_FragCoord.xy / czm_viewport.zw;
float globeDepth = getGlobeDepthAtCoords(fragSt);
doDepthTest(globeDepth);
vec4 color = texture(u_atlas, v_textureCoordinates); vec4 color = texture(u_atlas, v_textureCoordinates);
@ -243,6 +244,18 @@ void main()
out_FragColor = color; out_FragColor = color;
#ifdef LOG_DEPTH #ifdef LOG_DEPTH
czm_writeLogDepth(); // If we've made it here, we passed our manual depth test, above. But the automatic depth test will
// still run, and some fragments of the billboard may clip against the globe. To prevent that,
// ensure the depth value we write out is in front of the globe depth.
float depthArg = v_depthFromNearPlusOne;
if (globeDepth != 0.0) { // On the globe
float globeDepthFromNearPlusOne = (-globeDepth - czm_currentFrustum.x) + 1.0;
float nudge = max(globeDepthFromNearPlusOne * 5e-6, czm_epsilon7);
float globeOnTop = max(1.0, globeDepthFromNearPlusOne - nudge);
depthArg = min(depthArg, globeOnTop);
}
czm_writeLogDepth(depthArg);
#endif #endif
} }

File diff suppressed because it is too large Load Diff