-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Streamtubes [WIP] #701
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
Streamtubes [WIP] #701
Conversation
Notes on being a standalone
|
@@ -17,7 +17,7 @@ Mesh3D.colorbar = require('../heatmap/colorbar'); | |||
Mesh3D.plot = require('./convert'); | |||
|
|||
Mesh3D.moduleType = 'trace'; | |||
Mesh3D.name = 'mesh3d', | |||
Mesh3D.name = 'mesh3d'; |
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.
good 👀
Recap of discussion on decisions taken (correct me on the plotly.js slack or in the absence of that, as a comment below, if there's a misunderstanding and I'll update this comment):
|
Excellent. |
From @monfera
|
src/traces/streamtube/attributes.js
Outdated
width: scatterLineAttrs.width, | ||
connectionradius: extendFlat({}, scatterMarkerAttrs.size, { | ||
dflt: 1, | ||
description: 'Sets the radius of the line connection. Either a number, or an array with as many elements as the number of points.' |
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.
@monfera what are the units here?
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.
@etpinard it should be in terms of the data domain, e.g. in a 100 x 100 x 100 cube a radius of 1 should mean 1/100th of the box edge
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.
Great. This is perfectly reasonable mode (how should we call it? 'axis'
maybe?) In future development we could add other modes e.g. 'constant'
(which would make the streamtubes appear cylindrical regardless of the scene aspectratio) and 'data'
(which would correspond to a data array).
By the way, @monfera would you be ok with renaming connectionradius
-> size
or just radius
?
…nd marker sizes; use domain dimensions; name to streamtube etc.
line: extendFlat({}, { | ||
connectiondiameter: extendFlat({}, scatterMarkerAttrs.size, { | ||
dflt: 1, | ||
description: 'Sets the radius of the line connection. Either a number, or an array with as many elements as the number of points.' |
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.
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.
Yes. In this case, the light reflection can be more of a distraction than a positive. So with this type of use, it's good to avoid using a specular light component.
@monfera I've just noticed that the hover labels aren't being shown for every data point. Is that a known issue? |
@etpinard I don't think hover points show up and streamtubes in general are more of a continuous thing (e.g. wind flow) where points mostly serve as guides for where it curves and how radius, color evolve, rather than something like with scatterplots, so for this reason I haven't prioritized it. Currently, on hover, the crosshair lines wrap the mesh rather than focus on the central axis of the streamtube, which I believe would be the desired behavior (haven't yet had the time to do in the first iteration), and if that's done then on that basis it would probably be easy to cover the points (sphere centers) as well. So we could leave it with the mesh hugging crosshairs, or disable for now, or if it's a dependency, we could iterate on this. |
Will soon be superseded by https://github.com/gl-vis/gl-streamtube3d |
Goals
Streamtubes is a new trace type that gives plotly.js the ability to generate streamtubes. Inspirations:
Direct motivations included:
scatter3d
when point markers are of a different color than the linesImmediate goals: a
streamtubes
trace type, whichscatter3d
as possibleNice to have goals:
scatter3d
scatter3d
Previous steps
Development of #617 (now merged; example1 example2 example3 and see also the test cases in the changeset)
Receiving requirements on coloring, curvature and aesthetics (i.e. no Z-fighting)
The mentioned Z-fighting is an artifact that arises due to the pseudo-3D nature of

scatter3d
which uses much fewer polygons, but has inherent tradeoffs, see the arbitrary connections and glitches at the markers. Also observe the (sometimes desired) flat, non-3D like presentation and the sometimes muddy color gradients:Solution
We run a test atop of

mesh3d
and this showed the basic feasibility (realistic lighting, gradient color, curves) and led to management request for streamlines, as opposed to justscatter3d
with curves:Splines
Colors, radii need to be interpolated between points. Also, even high-density trace lines suffer from an angular look, so interpolation is needed to give a smooth surface. Moreover, it's desirable to give the option to smoothen the curve even if there are few points supported. Therefore a spline implementation had to be chosen (though more could be added in the future). Due to its favorable properties, Centripetal Catmull-Rom was chosen for its relative simplicity, computational speed, desirability for data visualization (respects monotonicity i.e. doesn't overshoot points; goes across all points; doesn't form weird loops), intuitive use (no need for control points other than user-supported points), and looks good. Also supported by the now-standard [http://bl.ocks.org/mbostock/1705868](D3 4.0) library and may have eventual use in animation from which it stems.
It wasn't possible to use the preexisting Catmull-Rom spline in
plotly.js
ord3.js
because they use Canvas and/or SVG and for efficiency of rendering, map Catmull-Rom to native splines (cubic, quadratic are native spanes). Also, we needed it to work in 3D, or more if we count the radius and color interpolations. (The current function is WIP and very un-DRY, but avoids memory allocation; to be refined later).Geometry
Each aspect of geometry is discussed in separation. From a bird's eye, we have markers and connections.

Marker geometry
Markers are currently icospheres, arrived at via increasing the LoD (level of detail) on an initial seed icosahedron. They are costly in that lots of polygons are being used for smooth appearance (compactness todo below). It's mitigated by the fact that rendering speed is usually limited by fill speed rather than geometry size. Also, the examples worked on so far posed no speed issue on rotation/zooming etc. even with a very high number of markers:

Here's an intentionally low-poly icosphere:


Other markers (umm.. damaged icospheres) were quickly tried but not yet added:
The icosphere points are shared (cached as the increase in LoD generates shared vertices).
Because icospheres are made up of a lot of polygons (involving quite a few calculations), but they don't need rotation, the solution defines a unit icosphere and marker rendering basically pastes this in with scaling and translation transforms only (trivial math).
Connection geometry - ring element
The simplest connection line would be a long, thin cylinder between two marker centers. But we need to vary the radius (in the future: cross-section too), and it's built out of polygons, so the solution uses the (generally non-right) frustum as the basic ring element. Lots of these glued to one another form the connecting line. In general, the frustum is not right, i.e. a curve makes it necessary that the pyramid cutting planes are at some infinitesimally small angle, to follow the curve. The cutting planes themselves are not rendered, only the sides, so there's an opening at the end of the 'tubes'.
Here's an example of low-poly, very long rings, that demonstrate not only the triangles that make up the frustum sides, but also how the gradient coloring works not only as a different color from ring to ring, but also, there's blending inside a ring (which reduces somewhat the number of polygons necessary to create for the same gradient smoothness). This tessellation in general should be invisible due to the infinitesimally short rings and small color changes.

Connection geometry - fusing the adjacent rings to form a tube
In order to not waste (duplicate) vertices between neighboring ring elements, the second cutting plane polygon vertices are being used as the first cutting plane poligon vertices in the next ring, i.e. it will form a joined mesh. This requires that attention is paid to the polygon point order, because, if the points are not in a matching order, then the ring will be degenerate (see the next point).
Gymbal lock
Each connection line ring is transformed in space (rotated and translated to its place). The current rotation approach is rather basic; it uses a distinguished normal vector around which the rotation takes place. This axis can be arbitrary but there is the degenerate case when the rotation vector is collinear to an arbitrarily picked normal vector. Therefore currently we loop through unit normal vectors (actually, x, y, z unit vectors for simplicity) and pick the first one that doesn't yield a degenerate case.
This unfortunately means that as the curvature evolves, the rings in a section of the curve are rotated as per an x normal vector, then another section by a y normal vector, so the above mentioned criterion for matching polygon points is violated, resulting in one degenerate ring. With fine enough resolution, it is either unnoticeable or shows up as a thin line (band) on the otherwise smooth surface. But it means that the connection line surface uses more polygons than visually needed.
The solution will be a conversion to quaternion based rotation which isn't vulnerable to the gymbal lock effect.
Mesh geometry in general
The generated mesh geometry internally is currently a tuple of
{[{x, y, z}], [{i, j, k}]
represented as long numerical vectors where {x, y, z}
are vertex coordinates and{i, j, k}
are faces.i
,j
,k
are the three vertices of a triangle. There's the straightforward optimization possibility that we generate amesh
rather than a set of triangles as currently, because it's easy to identify the adjacent polygons. In this case, each new vertex index is implied to form a triangle with the previous two vertex indices.There are numerous other optimization possibilities for geometry, including adaptively changing the LoD on zooming in parallel with culling, or doing some of the work not in geometry but shaders, but this initial PR reuses much of existing code.
Also, since the icospheres and tubes are meant to be closed hulls,
gl.CULL_FACE
could be enabled for them.Useful features from
scatter3d
: labeling and projectionThere's some visual disconnect between the realistic shapes and the flat-looking wall projections, and the label placement is just PoC, but it's a start:

Some examples
As the PR is right now, it generates this example (labels, color legend also present):

These below examples were generated during making a PoC, for example, straight linear interpolation isn't added yet, just the Catmull-Rom; closed loops aren't implemented (isn't hard to do).