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

Time Dynamic Point Clouds #6721

Merged
merged 41 commits into from
Jul 11, 2018
Merged

Time Dynamic Point Clouds #6721

merged 41 commits into from
Jul 11, 2018

Conversation

lilleyse
Copy link
Contributor

@lilleyse lilleyse commented Jun 22, 2018

Fixes #6397
This is built off of the Draco PR #6559, merge that first.

Adds the ability to playback time dynamic point cloud data. TimeDynamicPointCloud takes a TimeIntervalCollection, where each interval points to a 3D Tiles .pnts file and an optional transform.

Also added a new private class PointCloud that does the core loading and rendering of pnts content independent of a 3D Tileset. This was done so that PointCloud3DTileContent and TileDynamicPointCloud can both share the same core code even though one has a tileset and the other does not.

TileDynamicPointCloud has a lot of the same rendering features as a tileset - styling, clipping planes, attenuation, eye dome lighting, and maximum memory usage.

The playback works by calculating the average time it takes to load a frame and prefetching the next frame based on this average and how fast the clock is running. When that frame is reached it is rendered and another frame is prefetched. Any frames in-between are skipped. If the frames are sufficiently small or the clock is sufficiently slow then no frames will be skipped.

The alternative approach not implemented here is to load larger batches of frames and pause the simulation if a frame is not ready, similar to video buffering. This way no frames are skipped during playback. For the data I've been testing loading can rarely keep up with playback, so I expect the constant buffering would really hurt user experience.

The approach here is hopefully more flexible for a wider array of playback scenarios like scrubbing the timeline or setting the clock to a 5-100x speed. It will also adapt nicely to actual point cloud streaming, like real-time from a LiDAR sensor.

To do:

  • Unit tests
  • Update CHANGES.md
  • Calculate bounding volume for draw command and attenuation. The .pnts file does not contain a bounding volume.
    • May need to loop over point positions to do this.
  • Add image to Sandcastle
  • Move Sandcastle data to SampleData folder rather than Specs folder
  • Sharing GPU resources, vertex arrays, and render states (might be a task for after this PR)
  • When using Draco the first frame is far slower to load the rest. Don't include it in the average load time.
  • Make sure everything works with request render mode.
  • Handle shader changes across all frames
  • Update Sandcastle demo to not use CZML
  • Submit coding guide issue: Time Dynamic Point Clouds #6721 (comment)
  • Test behavior of failed requests

@cesium-concierge
Copy link

Signed CLA is on file.

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


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

🌍 🌎 🌏

@lilleyse lilleyse force-pushed the point-cloud-stream-new branch from bd5a049 to f505207 Compare June 22, 2018 18:20
@pjcozzi
Copy link
Contributor

pjcozzi commented Jun 22, 2018

If you didn't already include them, many of the notes in the top of this PR would make good code comments.

@@ -0,0 +1,77 @@
<!DOCTYPE html>
Copy link
Contributor

Choose a reason for hiding this comment

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

Trying to run this, I get "Request has failed. Status Code: 404"

Is this because the data is only in the specs?

http://cesium-dev.s3-website-us-east-1.amazonaws.com/cesium/point-cloud-stream-new/Apps/Sandcastle/index.html?src=Time%20Dynamic%20Point%20Cloud.html

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, that will be fixed.

var frames = properties.frames;
var intervals = frames.intervals;

// Update uris to be absolute
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a hack?

var basePath = '../../SampleData/test/PointCloudTimeDynamic/';
var czmlPath = basePath + 'frames.czml';
var dataSource = viewer.dataSources.add(Cesium.CzmlDataSource.load(czmlPath));
dataSource.then(function(dataSource) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should there be a helper for this?

What if a CZML has time-dynamic point clouds and other content - looks like it all just works, right?

var entities = dataSource.entities;
var entity = entities.getById('Time Dynamic Point Cloud');
var properties = entity.properties;
var frames = properties.frames;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the CZML spec be updated?

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 didn't actually make any changes to CZML in this PR, I just use CZML to package the data for the demo. CZML conveniently creates a TimeIntervalCollection under the hood that can be passed to TimeDynamicPointCloud.

But maybe I should hardcode the time intervals and uris in the demo itself to avoid the confusion here and #6721 (comment) #6721 (comment).

Copy link
Contributor

Choose a reason for hiding this comment

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

Hardcoding sounds OK; if CZML can now point to a .pnts file, we should update the spec for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No CZML spec updates required.

The demo is just using CZML as a storage mechanism and helper.

There was some offline discussion that this use case is too niche to be added to core CZML, so I never built that in. But the demo shows that if you had a czml with a custom property you could get a TimeIntervalCollection and initialize a TimeDynamicPointCloud. You can do this without CZML too, which is what I plan on doing in this demo instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, I suppose. We have someone relying on this and we need to provide them guidance on the best practice whether that be a custom CZML property or a completely separate container. They are already a CZML user as you know but that does not imply that the point clouds needs to be part of it.


createUniformMap(pointCloud, frameState);

var vs = 'attribute vec3 a_position; \n' +
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 current use case, how does the shader cache perform? It doesn't thrash right - since a shader program isn't deleted right away? Nonetheless it has to do a ton of string processing - could be a good roadmap or PERFORMANCE_IDEA to improve just like reusing a vertex buffer.

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, no shader cache thrashing but there is a bunch of string processing. This and other GPU resource optimizations are now in a PERFORMANCE_IDEA.

*
* @param {Object} options Object with the following properties:
* @param {Clock} options.clock A {@link Clock} instance that is used when determining the value for the time dimension.
* @param {TimeIntervalCollection} options.intervals A {@link TimeIntervalCollection} with its data property being an object containing a uri to a Point Cloud (.pnts) tile and an optional transform.
Copy link
Contributor

Choose a reason for hiding this comment

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

For context, probably say "3D Tiles Point Cloud"

'use strict';

/**
* Provides functionality for playback of time-dynamic point cloud data.
Copy link
Contributor

Choose a reason for hiding this comment

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

Style-wise I think "Provides functionality" is empty - could say that about anything.

* @param {Boolean} [options.show=true] Determines if the point cloud will be shown.
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] A 4x4 transformation matrix that transforms the point cloud.
* @param {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the point cloud casts or receives shadows from each light source.
* @param {Number} [options.maximumMemoryUsage=256] The maximum amount of memory in MB that can be used by the point cloud.
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps details for the property reference doc but how does this work if one frame exceeds the usage? I assume it is allowed. Reference doc should also briefly explain that this is streaming, prefectches, and uses an LRU cache like the GitHub comment mentions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One frame that exceeds the usage will still be rendered.

I added a note that should clarify this (similar to the Cesium3DTileset wording):

Frames that are not being loaded or rendered are unloaded to enforce this.

* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] A 4x4 transformation matrix that transforms the point cloud.
* @param {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the point cloud casts or receives shadows from each light source.
* @param {Number} [options.maximumMemoryUsage=256] The maximum amount of memory in MB that can be used by the point cloud.
* @param {Object} [options.pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point size based on geometric error and eye dome lighting.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should "geometric error" be here?

*
* @see TimeDynamicPointCloud#maximumMemoryUsage
*/
totalMemoryUsageInBytes : {
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 that some of these properties should be in MB and others in bytes? What does the rest of Cesium do?

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 copied what Cesium3DTileset does. I think the premise is any option that is settable from the user is in MB and anything internal is bytes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps submit an issue to add this to the Coding Guide? Or just update it.

@pjcozzi
Copy link
Contributor

pjcozzi commented Jun 22, 2018

If this is fast enough for the current use case, some of the TODOs at the top of this PR and ideas in #6397 could be a new roadmap issue or PERFORMANCE_IDEA comments even if allocating a new vertex buffer per frame makes me hurt inside. 😢

@lilleyse lilleyse force-pushed the point-cloud-stream-new branch from f79ff9a to f6dc8ee Compare June 27, 2018 01:24
@lilleyse lilleyse force-pushed the point-cloud-stream-new branch from b767800 to f531406 Compare July 3, 2018 17:23
@lilleyse
Copy link
Contributor Author

lilleyse commented Jul 3, 2018

@pjcozzi @ggetz Updated

@pjcozzi
Copy link
Contributor

pjcozzi commented Jul 3, 2018

@ggetz please run with it.

@ggetz ggetz self-assigned this Jul 9, 2018
@lilleyse lilleyse changed the base branch from point-clouds-draco to master July 9, 2018 14:46
@lilleyse lilleyse force-pushed the point-cloud-stream-new branch from 6161a96 to adf8f4e Compare July 9, 2018 15:00
@lilleyse
Copy link
Contributor Author

lilleyse commented Jul 9, 2018

@ggetz Updated and synced up with master.

Copy link
Contributor

@ggetz ggetz left a comment

Choose a reason for hiding this comment

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

I think there is an issue with bounding spheres when providing a transform. For instance, here's the initial zoomedTo view when I specify

function dataCallback(interval, index) {
    return {
        uri: uris[index],
        transform : Cesium.Matrix4.fromUniformScale(8.0)
    };
}

image


I'm having trouble specifying clipping planes because I need to get the bounding sphere center in order to transform them correctly.


Also nitpicky, but can we add more "time slices" to the Sandcastle thumbnail? It wasn't immediately obvious to me what it was trying to show with just the two.

<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<meta name="description" content="Time Dynamic Point Cloud">
<meta name="cesium-sandcastle-labels" content="Showcases, 3D Tiles">
Copy link
Contributor

Choose a reason for hiding this comment

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

Totally just semantics but technically, though it uses .pnts files, this isn't really 3D Tiles, is it?

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 was debating this too. I'll move it out of 3D Tiles.

byteOffset += sizeOfUint32; // Skip magic

var version = view.getUint32(byteOffset, true);
if (version !== 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should have asked this in the last PR, but should we check supported extensions like we do in Model.js?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm.. I'm tempted to skip for now because PointCloud and TimeDynamicPointCloud don't have a list of extensions used by the tile/tileset. It would have to guess where to look for unsupported extension, like in the feature table and batch table extensions object. But it feels a bit weird to do that at the tile level.

Extension checking probably belongs at the Cesium3DTileset level. Maybe worth adding to #6780?

var message = defined(error.message) ? error.message : error.toString();
if (that.frameFailed.numberOfListeners > 0) {
that.frameFailed.raiseEvent({
url : uri,
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 uri as well?

* console.log('Error: ' + error.message);
* });
*/
this.frameFailed = new Event();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the error properties be documented here as well.

that._runningSamples[that._runningIndex] = loadTime;
that._runningLength = Math.min(that._runningLength + 1, that._runningSamples.length);
that._runningIndex = (that._runningIndex + 1) % that._runningSamples.length;
that._runningAverage = that._runningSum / that._runningLength;
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 a reason we're replacing each frame with the last sample instead of just taking one long running average? I think it would be simpler to keep a running average with something like runningAverge = (previousAverage * n / (n + 1)) + loadTime / (n + 1) and you would only need to keep track of the runningAverage and the total number of samples.

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 initially had a long running average but extra slow frames would really throw off the average. Less noticeable in a simple app, but it became a big problem in the app I was testing where lots of other network requests were being made.

So now it just tracks the last N frames, and adapts a lot better to variations in tile size, network speed, etc.

return;
}

var clockMultiplierChanged = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

You may have already considered this, but would it make more sense to handle the current/previous/next interval logic in a function that subscribes to the clock tick event instead of in the render loop itself? This is what TimeDynamicImagery does.

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 think TimeDynamicImagery does that because it doesn't have an update function that gets called. For point clouds I like that all the update stuff is in the same place.

function getUnloadCondition(frameState) {
return function(frame) {
// Unload all frames that aren't currently being loaded or rendered
return frame.touchedFrameNumber < frameState.frameNumber;
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this also make sense if the clock is running in reverse?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FrameState's frameNumber is always incremented, not attached to the clock.

*
* @param {Object} options Object with the following properties:
* @param {Clock} options.clock A {@link Clock} instance that is used when determining the value for the time dimension.
* @param {TimeIntervalCollection} options.intervals A {@link TimeIntervalCollection} with its data property being an object containing a <code>uri</code> to a 3D Tiles Point Cloud tile and an optional <code>transform</code>.
Copy link
Contributor

Choose a reason for hiding this comment

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

<code>data</code> property

TimeDynamicPointCloud.prototype._getAverageLoadTime = function() {
if (this._runningLength === 0) {
// Before any frames have loaded make a best guess about the average load time
return 0.05;
Copy link
Contributor

Choose a reason for hiding this comment

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

What's this based on? Can we grab any properties from the browser or system to make a more specific guess?

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 basically arbitrary - tuned roughly to a 300kb Draco tile. We would need to know tile size before it's downloaded and network speed to make a proper guess. I don't think it matters too much since the average will get corrected once tiles start to load.

<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<meta name="description" content="Time Dynamic Point Cloud">
Copy link
Contributor

Choose a reason for hiding this comment

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

The description shouldn't just repeat the name.

@lilleyse
Copy link
Contributor Author

I think there is an issue with bounding spheres when providing a transform. For instance, here's the initial zoomedTo view when I specify

I think this is two problems:

  1. The tile uses an RTC_CENTER of [1215012.8828876738,-4736313.051199594,4081605.22126042] which is applied before the transform, so the transform is scaling a really large translation and pushing it really far away from the globe. A translation only transform should look more correct. I think this is expected behavior though.

  2. For pnts files that aren't quantized, the bounding volume is calculated by sampling 20 points (evenly spaced apart) in the point cloud here. The test data has an unlucky point layout which causes the bounding sphere to center near the corner. I didn't see this problem with real-world data. But to be safe I should probably sample randomly.

20 samples
bv1

Calculated from all points
bv2

@lilleyse lilleyse force-pushed the point-cloud-stream-new branch from 86f1dbb to 5476619 Compare July 10, 2018 03:03
@lilleyse lilleyse force-pushed the point-cloud-stream-new branch from 5476619 to cb9ee74 Compare July 10, 2018 03:07
@lilleyse
Copy link
Contributor Author

@ggetz Updated.

Copy link
Contributor

@ggetz ggetz left a comment

Choose a reason for hiding this comment

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

@lilleyse

Looking good! I have a few more comments, and you also have some failing tests.

// Use same random values across all runs
if (!defined(randomValues)) {
CesiumMath.setRandomNumberSeed(0);
randomValues = new Array(20);
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be new Array(samplesLength);?

* Do not call this function directly. This is documented just to
* list the exceptions that may be propagated when the scene is rendered:
* </p>
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Either mark as @private or document, here and throughout.

if (defined(frame) && (frame !== this._lastRenderedFrame)) {
if (that.frameChanged.numberOfListeners > 0) {
frameState.afterRender.push(function() {
that.frameChanged.raiseEvent(that);
Copy link
Contributor

Choose a reason for hiding this comment

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

Works well, however, in requestRender mode, the frames stop advancing and this event stops firing after each frame has been loaded once. I think this may be an argument for handling the frame advancement in a clock listener rather than in the render loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's true, and is probably the right thing to do, I'm just afraid that splitting the update logic into two areas might complicate the design, when in most cases request render mode won't be used for an app that uses this features.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand your point about wanting to keep it simple, but on the other hand, I think separating the update and render logic also has value.

The biggest concern I have is since the update is tied to the rendering, that event won't trigger when in request render mode, so there's no way to watch and explicitly render a new frame when needed. I can't attest to the use cases for this as much as you, but it seems like a good candidate for requestRender mode in that you only need to render a frame (assuming nothing else is changing) when the timeline advances enough. However if you think it won't get used, then this doesn't need to be a priority now.

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 think its best to revisit later if it comes up. The render and update are just too linked together at the moment, sometimes by necessity, and it might take some time to get the separation right.

@lilleyse
Copy link
Contributor Author

Updated. Hopefully b2b94f5 fixes the tests.

@ggetz
Copy link
Contributor

ggetz commented Jul 11, 2018

Ok, this looks good to me. Thanks @lilleyse !

@ggetz ggetz merged commit 4529694 into master Jul 11, 2018
@lilleyse lilleyse deleted the point-cloud-stream-new branch July 26, 2018 16:22
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.

4 participants