-
Notifications
You must be signed in to change notification settings - Fork 3.5k
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
Conversation
@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. 🌍 🌎 🌏 |
bd5a049
to
f505207
Compare
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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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' + |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 : { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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 |
…rage load time can't be determined
f79ff9a
to
f6dc8ee
Compare
b767800
to
f531406
Compare
@ggetz please run with it. |
6161a96
to
adf8f4e
Compare
@ggetz Updated and synced up with master. |
There was a problem hiding this 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)
};
}
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"> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>. |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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.
I think this is two problems:
|
86f1dbb
to
5476619
Compare
5476619
to
cb9ee74
Compare
@ggetz Updated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! I have a few more comments, and you also have some failing tests.
Source/Scene/PointCloud.js
Outdated
// Use same random values across all runs | ||
if (!defined(randomValues)) { | ||
CesiumMath.setRandomNumberSeed(0); | ||
randomValues = new Array(20); |
There was a problem hiding this comment.
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> | ||
*/ |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Updated. Hopefully b2b94f5 fixes the tests. |
Ok, this looks good to me. Thanks @lilleyse ! |
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 aTimeIntervalCollection
, 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 ofpnts
content independent of a 3D Tileset. This was done so thatPointCloud3DTileContent
andTileDynamicPointCloud
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: