diff --git a/Apps/Sandcastle/gallery/3D Tiles Feature Picking.html b/Apps/Sandcastle/gallery/3D Tiles Feature Picking.html
index 23ae523d1666..18f05ae35e33 100644
--- a/Apps/Sandcastle/gallery/3D Tiles Feature Picking.html
+++ b/Apps/Sandcastle/gallery/3D Tiles Feature Picking.html
@@ -37,6 +37,7 @@
terrain: Cesium.Terrain.fromWorldTerrain(),
});
+ let async = false;
viewer.scene.globe.depthTestAgainstTerrain = true;
// Set the initial camera view to look at Manhattan
@@ -133,6 +134,34 @@
return description;
}
+ Sandcastle.addToggleButton("Async Picking", async, function (checked) {
+ async = checked;
+ });
+
+ let picking = false;
+ let debounceTime = 0;
+ const DEBOUNCE_TIMEOUT_MS = 34; // One pick per frame (30FPS)
+
+ async function doPick(scene, position, debounce, async, result) {
+ const now = performance.now();
+ if (debounce && picking && now - debounceTime < DEBOUNCE_TIMEOUT_MS) {
+ return true;
+ }
+ if (debounce) {
+ picking = true;
+ debounceTime = now;
+ }
+ if (async) {
+ result[0] = await scene.pickAsync(position);
+ } else {
+ result[0] = scene.pick(position);
+ }
+ if (debounce) {
+ picking = false;
+ }
+ return false;
+ }
+
// If silhouettes are supported, silhouette features in blue on mouse over and silhouette green on mouse click.
// If silhouettes are not supported, change the feature color to yellow on mouse over and green on mouse click.
if (Cesium.PostProcessStageLibrary.isSilhouetteSupported(viewer.scene)) {
@@ -157,13 +186,26 @@
);
// Silhouette a feature blue on hover.
- viewer.screenSpaceEventHandler.setInputAction(function onMouseMove(movement) {
+ viewer.screenSpaceEventHandler.setInputAction(async function onMouseMove(
+ movement,
+ ) {
+ // Pick a new feature
+ let pickedFeature = [];
+ const debounced = await doPick(
+ viewer.scene,
+ movement.endPosition,
+ true,
+ async,
+ pickedFeature,
+ );
+ if (debounced) {
+ return;
+ }
+ pickedFeature = pickedFeature[0];
+
// If a feature was previously highlighted, undo the highlight
silhouetteBlue.selected = [];
- // Pick a new feature
- const pickedFeature = viewer.scene.pick(movement.endPosition);
-
updateNameOverlay(pickedFeature, movement.endPosition);
if (!Cesium.defined(pickedFeature)) {
@@ -177,12 +219,26 @@
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// Silhouette a feature on selection and show metadata in the InfoBox.
- viewer.screenSpaceEventHandler.setInputAction(function onLeftClick(movement) {
+ viewer.screenSpaceEventHandler.setInputAction(async function onLeftClick(
+ movement,
+ ) {
+ // Pick a new feature
+ let pickedFeature = [];
+ const debounced = await doPick(
+ viewer.scene,
+ movement.position,
+ false,
+ async,
+ pickedFeature,
+ );
+ if (debounced) {
+ return;
+ }
+ pickedFeature = pickedFeature[0];
+
// If a feature was previously selected, undo the highlight
silhouetteGreen.selected = [];
- // Pick a new feature
- const pickedFeature = viewer.scene.pick(movement.position);
if (!Cesium.defined(pickedFeature)) {
clickHandler(movement);
return;
@@ -216,14 +272,29 @@
};
// Color a feature yellow on hover.
- viewer.screenSpaceEventHandler.setInputAction(function onMouseMove(movement) {
+ viewer.screenSpaceEventHandler.setInputAction(async function onMouseMove(
+ movement,
+ ) {
+ // Pick a new feature
+ let pickedFeature = [];
+ const debounced = await doPick(
+ viewer.scene,
+ movement.endPosition,
+ true,
+ async,
+ pickedFeature,
+ );
+ if (debounced) {
+ return;
+ }
+ pickedFeature = pickedFeature[0];
+
// If a feature was previously highlighted, undo the highlight
if (Cesium.defined(highlighted.feature)) {
highlighted.feature.color = highlighted.originalColor;
highlighted.feature = undefined;
}
- // Pick a new feature
- const pickedFeature = viewer.scene.pick(movement.endPosition);
+
updateNameOverlay(pickedFeature, movement.endPosition);
if (!Cesium.defined(pickedFeature)) {
@@ -239,14 +310,29 @@
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// Color a feature on selection and show metadata in the InfoBox.
- viewer.screenSpaceEventHandler.setInputAction(function onLeftClick(movement) {
+ viewer.screenSpaceEventHandler.setInputAction(async function onLeftClick(
+ movement,
+ ) {
+ // Pick a new feature
+ let pickedFeature = [];
+ const debounced = await doPick(
+ viewer.scene,
+ movement.position,
+ false,
+ async,
+ pickedFeature,
+ );
+ if (debounced) {
+ return;
+ }
+ pickedFeature = pickedFeature[0];
+
// If a feature was previously selected, undo the highlight
if (Cesium.defined(selected.feature)) {
selected.feature.color = selected.originalColor;
selected.feature = undefined;
}
- // Pick a new feature
- const pickedFeature = viewer.scene.pick(movement.position);
+
if (!Cesium.defined(pickedFeature)) {
clickHandler(movement);
return;
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 19bb2bd48111..91867694260d 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -104,6 +104,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
- [Erin Ingram](https://github.com/eringram)
- [Daniel Zhong](https://github.com/danielzhong)
- [Mark Schlosser](https://github.com/markschlosseratbentley)
+ - [Adam Larkeryd](https://github.com/alarkbentley)
- [Flightradar24 AB](https://www.flightradar24.com)
- [Aleksei Kalmykov](https://github.com/kalmykov)
- [BIT Systems](http://www.caci.com/bit-systems)
diff --git a/packages/engine/Source/Renderer/Buffer.js b/packages/engine/Source/Renderer/Buffer.js
index 039650950ccb..6e67f26ffa27 100644
--- a/packages/engine/Source/Renderer/Buffer.js
+++ b/packages/engine/Source/Renderer/Buffer.js
@@ -72,6 +72,24 @@ function Buffer(options) {
this.vertexArrayDestroyable = true;
}
+Buffer.createPixelBuffer = function (options) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined("options.context", options.context);
+ //>>includeEnd('debug');
+
+ if (!options.context._webgl2) {
+ throw new DeveloperError("A WebGL 2 context is required.");
+ }
+
+ return new Buffer({
+ context: options.context,
+ bufferTarget: WebGLConstants.PIXEL_PACK_BUFFER,
+ typedArray: options.typedArray,
+ sizeInBytes: options.sizeInBytes,
+ usage: options.usage,
+ });
+};
+
/**
* Creates a vertex buffer, which contains untyped vertex data in GPU-controlled memory.
*
@@ -242,6 +260,18 @@ Buffer.prototype._getBuffer = function () {
return this._buffer;
};
+Buffer.prototype._bind = function () {
+ const gl = this._gl;
+ const target = this._bufferTarget;
+ gl.bindBuffer(target, this._buffer);
+};
+
+Buffer.prototype._unBind = function () {
+ const gl = this._gl;
+ const target = this._bufferTarget;
+ gl.bindBuffer(target, null);
+};
+
Buffer.prototype.copyFromArrayView = function (arrayView, offsetInBytes) {
offsetInBytes = offsetInBytes ?? 0;
diff --git a/packages/engine/Source/Renderer/BufferUsage.js b/packages/engine/Source/Renderer/BufferUsage.js
index a346938793f1..dc9b3727680e 100644
--- a/packages/engine/Source/Renderer/BufferUsage.js
+++ b/packages/engine/Source/Renderer/BufferUsage.js
@@ -7,12 +7,14 @@ const BufferUsage = {
STREAM_DRAW: WebGLConstants.STREAM_DRAW,
STATIC_DRAW: WebGLConstants.STATIC_DRAW,
DYNAMIC_DRAW: WebGLConstants.DYNAMIC_DRAW,
+ DYNAMIC_READ: WebGLConstants.DYNAMIC_READ,
validate: function (bufferUsage) {
return (
bufferUsage === BufferUsage.STREAM_DRAW ||
bufferUsage === BufferUsage.STATIC_DRAW ||
- bufferUsage === BufferUsage.DYNAMIC_DRAW
+ bufferUsage === BufferUsage.DYNAMIC_DRAW ||
+ bufferUsage === BufferUsage.DYNAMIC_READ
);
},
};
diff --git a/packages/engine/Source/Renderer/Context.js b/packages/engine/Source/Renderer/Context.js
index b5d4d83a2a2f..9df2769ec76f 100644
--- a/packages/engine/Source/Renderer/Context.js
+++ b/packages/engine/Source/Renderer/Context.js
@@ -1,3 +1,4 @@
+import Buffer from "./Buffer.js";
import Check from "../Core/Check.js";
import Color from "../Core/Color.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
@@ -1449,6 +1450,7 @@ Context.prototype.endFrame = function () {
* @param {number} [readState.width=this.drawingBufferWidth] The width of the rectangle to read from.
* @param {number} [readState.height=this.drawingBufferHeight] The height of the rectangle to read from.
* @param {Framebuffer} [readState.framebuffer] The framebuffer to read from. If undefined, the read will be from the default framebuffer.
+ * @param {Framebuffer} [readState.pbo] If true pixel data is read to PBO instead of TypedArray.
* @returns {Uint8Array|Uint16Array|Float32Array|Uint32Array} The pixels in the specified rectangle.
*/
Context.prototype.readPixels = function (readState) {
@@ -1460,6 +1462,11 @@ Context.prototype.readPixels = function (readState) {
const width = readState.width ?? this.drawingBufferWidth;
const height = readState.height ?? this.drawingBufferHeight;
const framebuffer = readState.framebuffer;
+ const pbo = readState.pbo ?? false;
+
+ if (pbo && !this._webgl2) {
+ throw new DeveloperError("A WebGL 2 context is required.");
+ }
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThan("readState.width", width, 0);
@@ -1467,28 +1474,58 @@ Context.prototype.readPixels = function (readState) {
//>>includeEnd('debug');
let pixelDatatype = PixelDatatype.UNSIGNED_BYTE;
+ let pixelFormat = PixelFormat.RGBA;
if (defined(framebuffer) && framebuffer.numberOfColorAttachments > 0) {
pixelDatatype = framebuffer.getColorTexture(0).pixelDatatype;
+ pixelFormat = framebuffer.getColorTexture(0).pixelFormat;
}
- const pixels = PixelFormat.createTypedArray(
- PixelFormat.RGBA,
- pixelDatatype,
- width,
- height,
- );
+ let pixels;
+ if (pbo) {
+ pixels = Buffer.createPixelBuffer({
+ context: this,
+ sizeInBytes: PixelFormat.textureSizeInBytes(
+ pixelFormat,
+ pixelDatatype,
+ width,
+ height,
+ ),
+ usage: BufferUsage.DYNAMIC_READ,
+ });
+ } else {
+ pixels = PixelFormat.createTypedArray(
+ pixelFormat,
+ pixelDatatype,
+ width,
+ height,
+ );
+ }
bindFramebuffer(this, framebuffer);
- gl.readPixels(
- x,
- y,
- width,
- height,
- PixelFormat.RGBA,
- PixelDatatype.toWebGLConstant(pixelDatatype, this),
- pixels,
- );
+ if (pbo) {
+ pixels._bind();
+ gl.readPixels(
+ x,
+ y,
+ width,
+ height,
+ pixelFormat,
+ PixelDatatype.toWebGLConstant(pixelDatatype, this),
+ 0,
+ );
+ pixels._unBind();
+ } else {
+ gl.readPixels(
+ x,
+ y,
+ width,
+ height,
+ PixelFormat.RGBA,
+ PixelDatatype.toWebGLConstant(pixelDatatype, this),
+ pixels,
+ );
+ }
return pixels;
};
diff --git a/packages/engine/Source/Renderer/Sync.js b/packages/engine/Source/Renderer/Sync.js
new file mode 100644
index 000000000000..237f57c2fa0b
--- /dev/null
+++ b/packages/engine/Source/Renderer/Sync.js
@@ -0,0 +1,46 @@
+import Check from "../Core/Check.js";
+import destroyObject from "../Core/destroyObject.js";
+import DeveloperError from "../Core/DeveloperError.js";
+import Frozen from "../Core/Frozen.js";
+import WebGLConstants from "../Core/WebGLConstants.js";
+
+/**
+ * @private
+ */
+function Sync(options) {
+ options = options ?? Frozen.EMPTY_OBJECT;
+
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined("options.context", options.context);
+ //>>includeEnd('debug');
+
+ if (!options.context._webgl2) {
+ throw new DeveloperError("A WebGL 2 context is required.");
+ }
+
+ const context = options.context;
+ const gl = context._gl;
+
+ const sync = gl.fenceSync(WebGLConstants.SYNC_GPU_COMMANDS_COMPLETE, 0);
+
+ this._gl = gl;
+ this._sync = sync;
+}
+Sync.create = function (options) {
+ return new Sync(options);
+};
+Sync.prototype.getStatus = function () {
+ const status = this._gl.getSyncParameter(
+ this._sync,
+ WebGLConstants.SYNC_STATUS,
+ );
+ return status;
+};
+Sync.prototype.isDestroyed = function () {
+ return false;
+};
+Sync.prototype.destroy = function () {
+ this._gl.deleteSync(this._sync);
+ return destroyObject(this);
+};
+export default Sync;
diff --git a/packages/engine/Source/Scene/PickFramebuffer.js b/packages/engine/Source/Scene/PickFramebuffer.js
index b6884933b0d2..33736a026d8b 100644
--- a/packages/engine/Source/Scene/PickFramebuffer.js
+++ b/packages/engine/Source/Scene/PickFramebuffer.js
@@ -4,6 +4,10 @@ import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import FramebufferManager from "../Renderer/FramebufferManager.js";
import PassState from "../Renderer/PassState.js";
+import PixelDatatype from "../Renderer/PixelDatatype.js";
+import PixelFormat from "../Core/PixelFormat.js";
+import WebGLConstants from "../Core/WebGLConstants.js";
+import Sync from "../Renderer/Sync.js";
/**
* @private
@@ -27,47 +31,18 @@ function PickFramebuffer(context) {
this._height = 0;
}
-PickFramebuffer.prototype.begin = function (screenSpaceRectangle, viewport) {
- const context = this._context;
- const { width, height } = viewport;
-
- BoundingRectangle.clone(
- screenSpaceRectangle,
- this._passState.scissorTest.rectangle,
- );
-
- // Create or recreate renderbuffers and framebuffer used for picking
- this._width = width;
- this._height = height;
- this._fb.update(context, width, height);
- this._passState.framebuffer = this._fb.framebuffer;
-
- this._passState.viewport.width = width;
- this._passState.viewport.height = height;
-
- return this._passState;
-};
-
/**
* Return the picked objects rendered within a given rectangle.
*
- * @param {BoundingRectangle} screenSpaceRectangle
+ * @private
+ * @param {object} context The active context.
+ * @param {Uint8Array|Uint16Array|Float32Array|Uint32Array} pixels The pixels in the specified scratch rectangle.
+ * @param {number} width The scratch rectangle width.
+ * @param {number} height The scratch rectangle height.
* @param {number} [limit=1] If supplied, stop iterating after collecting this many objects.
* @returns {object[]} A list of rendered objects, ordered by distance to the middle of the rectangle.
*/
-PickFramebuffer.prototype.end = function (screenSpaceRectangle, limit = 1) {
- const width = screenSpaceRectangle.width ?? 1.0;
- const height = screenSpaceRectangle.height ?? 1.0;
-
- const context = this._context;
- const pixels = context.readPixels({
- x: screenSpaceRectangle.x,
- y: screenSpaceRectangle.y,
- width: width,
- height: height,
- framebuffer: this._fb.framebuffer,
- });
-
+function colorScratchForObject(context, pixels, width, height, limit = 1) {
const max = Math.max(width, height);
const length = max * max;
const halfWidth = Math.floor(width * 0.5);
@@ -121,6 +96,152 @@ PickFramebuffer.prototype.end = function (screenSpaceRectangle, limit = 1) {
y += dy;
}
return [...objects];
+}
+
+/**
+ * Creates a callback function that will once per frame poll the Sync state until signaled.
+ *
+ * @private
+ * @param {object} pickState An object with the following properties:
+ * @param {number} [readState.frameState] The active framestate.
+ * @param {number} [readState.frameNumber] The current frame number.
+ * @param {number} [readState.sync] The Sync object to poll.
+ * @param {number} [readState.ttl=10] Max number of frames to poll until reject.
+ * @param {function} onSignalCallback Callback to execute on Sync Signal.
+ */
+function createAsyncPick(pickState, onSignalCallback) {
+ return () => {
+ const sync = pickState.sync;
+ const frameState = pickState.frameState;
+ const ttl = pickState.ttl ?? 10;
+ const syncStatus = sync.getStatus();
+ const signaled = syncStatus === WebGLConstants.SIGNALED;
+ const frameDelta = frameState.frameNumber - pickState.frameNumber; // how many frames passed since inital request
+ if (signaled || frameDelta > ttl) {
+ sync.destroy();
+ onSignalCallback(signaled);
+ } else {
+ frameState.afterRender.push(createAsyncPick(pickState, onSignalCallback));
+ }
+ };
+}
+
+PickFramebuffer.prototype.begin = function (screenSpaceRectangle, viewport) {
+ const context = this._context;
+ const { width, height } = viewport;
+
+ BoundingRectangle.clone(
+ screenSpaceRectangle,
+ this._passState.scissorTest.rectangle,
+ );
+
+ // Create or recreate renderbuffers and framebuffer used for picking
+ this._width = width;
+ this._height = height;
+ this._fb.update(context, width, height);
+ this._passState.framebuffer = this._fb.framebuffer;
+
+ this._passState.viewport.width = width;
+ this._passState.viewport.height = height;
+
+ return this._passState;
+};
+
+/**
+ * Return the picked objects rendered within a given rectangle using asynchronously without stalling the GPU.
+ *
+ * @param {BoundingRectangle} screenSpaceRectangle
+ * @param {FrameState} frameState
+ * @param {number} [limit=1] If supplied, stop iterating after collecting this many objects.
+ * @returns {object[]} A list of rendered objects, ordered by distance to the middle of the rectangle.
+ */
+PickFramebuffer.prototype.endAsync = async function (
+ screenSpaceRectangle,
+ frameState,
+ limit = 1,
+) {
+ const width = screenSpaceRectangle.width ?? 1.0;
+ const height = screenSpaceRectangle.height ?? 1.0;
+
+ const context = this._context;
+ const framebuffer = this._fb.framebuffer;
+
+ let pixelDatatype = PixelDatatype.UNSIGNED_BYTE;
+ let pixelFormat = PixelFormat.RGBA;
+
+ if (defined(framebuffer) && framebuffer.numberOfColorAttachments > 0) {
+ pixelDatatype = framebuffer.getColorTexture(0).pixelDatatype;
+ pixelFormat = framebuffer.getColorTexture(0).pixelFormat;
+ }
+
+ const pbo = context.readPixels({
+ x: screenSpaceRectangle.x,
+ y: screenSpaceRectangle.y,
+ width: width,
+ height: height,
+ framebuffer: framebuffer,
+ pbo: true,
+ });
+
+ const sync = Sync.create({
+ context: context,
+ });
+
+ const pickState = {
+ frameState: frameState,
+ frameNumber: frameState.frameNumber,
+ sync: sync,
+ };
+
+ return new Promise((resolve, reject) => {
+ frameState.afterRender.push(
+ createAsyncPick(pickState, (signaled) => {
+ const pixels = PixelFormat.createTypedArray(
+ pixelFormat,
+ pixelDatatype,
+ width,
+ height,
+ );
+ pbo.getBufferData(pixels);
+ pbo.destroy();
+ const pickedObjects = colorScratchForObject(
+ context,
+ pixels,
+ width,
+ height,
+ limit,
+ );
+ if (signaled) {
+ resolve(pickedObjects);
+ } else {
+ reject("Picking Request Timeout");
+ }
+ }),
+ );
+ });
+};
+
+/**
+ * Return the picked objects rendered within a given rectangle.
+ *
+ * @param {BoundingRectangle} screenSpaceRectangle
+ * @param {number} [limit=1] If supplied, stop iterating after collecting this many objects.
+ * @returns {object[]} A list of rendered objects, ordered by distance to the middle of the rectangle.
+ */
+PickFramebuffer.prototype.end = function (screenSpaceRectangle, limit = 1) {
+ const width = screenSpaceRectangle.width ?? 1.0;
+ const height = screenSpaceRectangle.height ?? 1.0;
+
+ const context = this._context;
+ const pixels = context.readPixels({
+ x: screenSpaceRectangle.x,
+ y: screenSpaceRectangle.y,
+ width: width,
+ height: height,
+ framebuffer: this._fb.framebuffer,
+ });
+
+ return colorScratchForObject(context, pixels, width, height, limit);
};
/**
diff --git a/packages/engine/Source/Scene/Picking.js b/packages/engine/Source/Scene/Picking.js
index dc1ff2899179..51510eedff6a 100644
--- a/packages/engine/Source/Scene/Picking.js
+++ b/packages/engine/Source/Scene/Picking.js
@@ -273,7 +273,8 @@ function computePickingDrawingBufferRectangle(
* @param {number} [width=3] Width of the pick rectangle.
* @param {number} [height=3] Height of the pick rectangle.
* @param {number} [limit=1] If supplied, stop iterating after collecting this many objects.
- * @returns {object[]} List of objects containing the picked primitives.
+ * @param {boolean} [async=false] Use async non GPU blocking picking.
+ * @returns {object[] | Promise