drawing lots of identical features — what's the best approach? - performance

I need to draw a large number of rectangles (potentially up to millions), which are located all over the world. I was wondering what the optimal approach is to achieve the best possible performance. my requirements are:
all items are rectangles (not squares), and identical in size and color
their rotation is different per individual item though
they have different fixed locations – they do not move
the rectangles need to be pickable
they might need to be scaled according to current zoom level (to make them look like real objects on the ground)
use the webgl renderer
what I have tried to far:
const features = R.map(
(i) => {
// [...] calculate `coords` and `rotation`
const point = new ol.geom.Point(coords);
const feature = new ol.Feature(point);
feature.__angle = rotation;
return feature;
},
R.range(0, count /* lots of them! */)
);
const sheetStyle = new ol.style.Style({
image: new ol.style.Icon({
size: [5, 8], // shape of rectangle
src: 'color.png' // 1×1px image
})
});
const vectorLayer = new ol.layer.Vector({
source: new ol.source.Vector({ features }),
preload: Infinity,
updateWhileAnimating: true,
updateWhileInteracting: true,
style: (feature, resolution) => {
const image = sheetStyle.getImage();
// TODO: is there a way to only have to do this once?
image.setRotation(feature.__angle);
// scale according to zoom level
image.setScale(0.3 / resolution);
return sheetStyle;
},
});
I was wondering if ol3 was doing any sort of optimization under the hood.
does it merge the geometries into one?
does it only display items that in the visible part of the map?
since all items are identical, is there a way to use instancing?
related: for better performance, I am only creating a single style object that I am reusing for all items. however I need to set a rotation on each of them, which is why I am using a style function. once it is set, the rotation won't change anymore though. is there a way around having to call the style function every frame?
I am also considering using a heatmap layer for lower zoom levels and then switching to the vector layer as the user zooms in.
it would be great if someone could give me hints for overall performance improvements.

Related

Three.js: Merge BufferGeometries while keeping attribute position separate for each geometry

In the application I develop I am trying to combine the closest dots of the same color, dots created from a 3D coordinates vector and using Points, by using a BufferGeometry with custom positions, positions set by using the setAttribute command of the geometry and giving it a Float32BufferAttribute(positionArray, 3) object. The problem I encountered is that I have a lot of such geometries (tens of thousands usually) and since I add each one separately to the group, I have big performance issues.
So I tried to merge the buffer geometries in a single one to draw all of them at once using BufferGeometryUtils.mergeBufferGeometries, but that didn't work.
How it looks without merging the geometries
How it looks with merged geometries
This is how I create the geometries:
const newGeometry = baseGeometry.clone();
newGeometry.setAttribute(
'position',
new Float32BufferAttribute(geometryPositions, 3)
);
newGeometry.setAttribute(
'color',
new Float32BufferAttribute(geometryColors, 3)
);
newGeometry.computeBoundingSphere();
geometryArray.push(newGeometry);
And I add them like this to my group.
geometryArray.forEach((e) => {
this.group.add(new Mesh(e, baseMaterial));
});
This is how I merge them and add them to the group.
const merged = BufferGeometryUtils.mergeBufferGeometries(geometryArray);
this.group.add(new Mesh(merged, baseMaterial));
As you can see the geometries use the same material in all cases, the color being defined in the colors attribute of each geometry and vertexColors is set to true on the MeshBasicMaterial.
For a single geometry in the array, the position/color data looks like this. The sizes can be random, and the array may or may not be empty depending if the points have neighbors. The format is 3D coordinates [x1,y1,z1,x2,y2,z2,.....].
const positions = {
itemSize: 3,
type: 'Float32Array',
array: [
4118.44775390625, -839.14404296875, 845.7374877929688, 4125.9306640625,
-808.6709594726562, 856.7002563476562, 4118.44775390625,
-839.14404296875, 845.7374877929688, 4129.93017578125, -870.6640625,
828.08154296875,
],
normalized: false,
};
const colors = {
itemSize: 3,
type: 'Float32Array',
array: [
0.9725490212440491, 0.5960784554481506, 0.03529411926865578,
0.9725490212440491, 0.5960784554481506, 0.03529411926865578,
0.9725490212440491, 0.5960784554481506, 0.03529411926865578,
0.9725490212440491, 0.5960784554481506, 0.03529411926865578,
],
normalized: false,
};
How could I improve the performance of the code above and keeping the custom positions and colors of each geometry intact?

Is it possible to create a line with occasional discontinuities or a line comprising multiple independent strips in three.js?

I have a function that generates a series of points to define a line, but the function occasionally needs to "lift the pen" and then continue drawing from a new location. I would like to minimize the coding overhead associated with lifting the pen. I would also like to avoid the performance penalty of doubling up the number of points and using "LineSegments".
Is there any way to specify that I want the pen be lifted between specific pairs of points in the strip? Is there any other simple solution to this problem?
This is the code that I have now...
const tetherPoints = myDrawingFunction()
const tetherGeometry = new THREE.BufferGeometry().setFromPoints(tetherPoints)
var tetherMaterial = new THREE.LineBasicMaterial({
vertexColors: THREE.VertexColors,
color: 0x4897f8, // Note: This attribute does not seem to have any effect
transparent: true,
opacity: dParam.tetherVisibility
})
const tempTether = new THREE.Line(tetherGeometry, tetherMaterial)
scene.add(tempTether)

Is there a simple way of handling (transforming) a group of objects in SkiaSharp?

In a nutshell, let's say, I need to draw a complex object (arrow) which consists of certain amount of objects, like five (or more) lines, for instance. And what's more important, that object must be transformed with particular (dynamic) coordinates (including scaling, possibly).
My question is whether SkiaSharp has anything which I can use for manipulating of this complex object transformation (some sort of grouping etc.) or do I still need to calculate every single point manually (with matrix, for instance).
This question is related particularly to SkiaSharp as I use it on Xamarin, but maybe some general answers from Skia can also help with it?
I think, the question might be too common (and possibly not for stackoverflow exactly), but I just can't find any specific information in google.
Yes, I know how to use SkiaSharp for drawing primitives.
create an SKPath and add lines and other shapes to it
SKPath path = new SKPath();
path.LineTo(...);
...
...
then draw the SKPath on your canvas
canvas.DrawPath(path,paint);
you can apply a transform to the entire path before drawing
var rot = new SKMatrix();
SKMatrix.RotateDegrees(ref rot, 45.0f);
path.Transform(rot);
If you are drawing something more complex than a path SKPicture is perfect for this. You can set it up so that you construct it once and then reuse it easily and efficiently. In the example below, the SKPicture's origin is in the center of a 100 x 100 rectangle but that is arbitrary.
SKPicture myPicture;
SKPicture MyPicture {
get {
if(myPicture != null) {
return myPicture;
}
using(SKPictureRecorder recorder = new SKPictureRecorder())
using(SKCanvas canvas = recorder.BeginRecording(new SKRect(-50, -50, 50, 50)))
// draw using primitives
...
myPicture = recorder.EndRecording();
}
return myPicture;
}
}
Then you apply your transforms to the canvas, draw the picture and restore the canvas state. offsetX and offsetY correspond to where the origin of the SKPicture will be rendered.
canvas.Save();
canvas.Translate(offsetX, offsetY);
canvas.Scale(scaleAmount);
canvas.RotateDegrees(degrees);
canvas.DrawPicture(MyPicture);
canvas.Restore();

Display Mesh On Top Of Another | Remove Overalapping | Render Order | Three.js

I have 2 obj meshes.
They both have some common areas but not completely.
I displayed them both by adding them to screen ..
Just like a mesh on top of another.
But the lower mesh overlaps the top mesh
But what I want to acheive is the lower mesh should always stay below without overlapping and giving the space to the entire top mesh.
I went through this fiddle..Fiddle with renderorder
And I tried something with this like..
var objLoader1 = new OBJLoader2();
objLoader1.load('assets/object1.obj', (root) => {
root.renderOrder = 0;
scene.add(root);
});
var objLoader2 = new OBJLoader2();
objLoader2.load('assets/object2.obj', (root) => {
root.renderOrder = 1;
scene.add(root);
});
But I don't know for what reason the overlap still stays ..
I tried...
var objLoader1 = new OBJLoader2();
objLoader1.load('assets/object1.obj', (root) => {
objLoader1.renderOrder = 0;
scene.add(root);
});
var objLoader2 = new OBJLoader2();
objLoader2.load('assets/object2.obj', (root) => {
objLoader2.renderOrder = 1;
scene.add(root);
});
Then I tried going through this Fiddle .. Another Fiddle
But when I run in I get only the lower or the upper mesh .
But I want to see both without any overlaps..
var layer1 = new Layer(camera);
composer.addPass(layer1.renderPass);
layer1.scene.add(new THREE.AmbientLight(0xFFFFFF));
var objLoader1 = new OBJLoader2();
objLoader1.load('assets/object1.obj', (root) => {
layer1.scene.add(root);
});
var layer2 = new Layer(camera);
composer.addPass(layer2.renderPass);
layer2.scene.add(new THREE.AmbientLight(0xFFFFFF));
var objLoader2 = new OBJLoader2();
objLoader2.load('assets/object2.obj', (root) => {
layer2.scene.add(root);
});
I made the material depthTest to False
But Nothing Helped..
Can anyone help me achieve what I wanted ..
If anyone couldn't figure what I mean by overlapping see the image below..
And Thanks to anyone who took time and effort to go through and help me...
You can use polygonOffset to achieve your goal, which modifies the depth value right before a fragment is written to help move polygons off of eachother without visually changing the position:
material.polygonOffset = true;
material.polygonOffsetUnit = 1;
material.polygonOffsetFactor = 1;
Here is a fiddle demonstrating the solution:
https://jsfiddle.net/5s8ey0ad/1/
Here is what the OpenGL Docs have to say about polygon offset:
When GL_POLYGON_OFFSET_FILL, GL_POLYGON_OFFSET_LINE, or GL_POLYGON_OFFSET_POINT is enabled, each fragment's depth value will be offset after it is interpolated from the depth values of the appropriate vertices. The value of the offset is factor×DZ+r×units, where DZ is a measurement of the change in depth relative to the screen area of the polygon, and r is the smallest value that is guaranteed to produce a resolvable offset for a given implementation. The offset is added before the depth test is performed and before the value is written into the depth buffer.
You're experiencing z-fighting, which is when two or more planes occupy the same space in the depthBuffer, so the renderer doesn't know which one to render on top of the other. Render order alone doesn't fix this because they're both still on the same plane, regardless of which one gets drawn first. You have a few options to resolve this:
Move one of the beams ever so slightly up in the y-axis. A tiny fraction would give one priority over the other, and this distance may not be noticeable to the eye.
I saw your fiddle, and you forgot to add depthTest: false to your material. However, this will cause issues when depth-testing the rest of the shape, since some white is on top of the red, but also some red is on top of the white. The approach in the fiddle works only when it's a simple plane, not more complex geometries.
You can use a boolean operation that removes one shape from the other, like CSG.
I think you'd save yourself a lot of headache by using approach #1.

d3 synchronizing 2 separate zoom behaviors

I have the following d3/d3fc chart
https://codepen.io/parliament718/pen/BaNQPXx
The chart has a zoom behavior for the main area and a separate zoom behavior for the y-axis.
The y-axis can be dragged to rescale.
The problem I'm having trouble solving is that after dragging the y-axis to rescale and then subsequently panning the chart, there is a "jump" in the chart.
Obviously the 2 zoom behaviors have a disconnect and need to be synchronized but I'm racking my brain trying to fix this.
const mainZoom = zoom()
.on('zoom', () => {
xScale.domain(t.rescaleX(x2).domain());
yScale.domain(t.rescaleY(y2).domain());
});
const yAxisZoom = zoom()
.on('zoom', () => {
const t = event.transform;
yScale.domain(t.rescaleY(y2).domain());
render();
});
const yAxisDrag = drag()
.on('drag', (args) => {
const factor = Math.pow(2, -event.dy * 0.01);
plotArea.call(yAxisZoom.scaleBy, factor);
});
The desired behavior is for zooming, panning, and/or rescaling the axis to always apply the transformation from wherever the previous action finished, without any "jumps".
OK, so I've had another go at this - as mentioned in my previous answer, the biggest issue you need to overcome is that the d3-zoom only permits symmetrical scaling. This is something that has been widely discussed, and I believe Mike Bostock is addressing this in the next release.
So, in order to overcome the issue, you need to use multiple zoom behaviour. I have created a chart that has three, one for each axis and one for the plot area. The X & Y zoom behaviours are used to scale the axes. Whenever a zoom event is raised by the X & Y zoom behaviours, their translation values are copied across to the plot area. Likewise, when a translation occurs on the plot area, the x & y components are copied to the respective axis behaviours.
Scaling on the plot area is a little more complicated as we need to maintain the aspect ratio. In order to achieve this I store the previous zoom transform and use the scale delta to work out a suitable scale to apply to the X & Y zoom behaviours.
For convenience, I've wrapped all of this up into a chart component:
const interactiveChart = (xScale, yScale) => {
const zoom = d3.zoom();
const xZoom = d3.zoom();
const yZoom = d3.zoom();
const chart = fc.chartCartesian(xScale, yScale).decorate(sel => {
const plotAreaNode = sel.select(".plot-area").node();
const xAxisNode = sel.select(".x-axis").node();
const yAxisNode = sel.select(".y-axis").node();
const applyTransform = () => {
// apply the zoom transform from the x-scale
xScale.domain(
d3
.zoomTransform(xAxisNode)
.rescaleX(xScaleOriginal)
.domain()
);
// apply the zoom transform from the y-scale
yScale.domain(
d3
.zoomTransform(yAxisNode)
.rescaleY(yScaleOriginal)
.domain()
);
sel.node().requestRedraw();
};
zoom.on("zoom", () => {
// compute how much the user has zoomed since the last event
const factor = (plotAreaNode.__zoom.k - plotAreaNode.__zoomOld.k) / plotAreaNode.__zoomOld.k;
plotAreaNode.__zoomOld = plotAreaNode.__zoom;
// apply scale to the x & y axis, maintaining their aspect ratio
xAxisNode.__zoom.k = xAxisNode.__zoom.k * (1 + factor);
yAxisNode.__zoom.k = yAxisNode.__zoom.k * (1 + factor);
// apply transform
xAxisNode.__zoom.x = d3.zoomTransform(plotAreaNode).x;
yAxisNode.__zoom.y = d3.zoomTransform(plotAreaNode).y;
applyTransform();
});
xZoom.on("zoom", () => {
plotAreaNode.__zoom.x = d3.zoomTransform(xAxisNode).x;
applyTransform();
});
yZoom.on("zoom", () => {
plotAreaNode.__zoom.y = d3.zoomTransform(yAxisNode).y;
applyTransform();
});
sel
.enter()
.select(".plot-area")
.on("measure.range", () => {
xScaleOriginal.range([0, d3.event.detail.width]);
yScaleOriginal.range([d3.event.detail.height, 0]);
})
.call(zoom);
plotAreaNode.__zoomOld = plotAreaNode.__zoom;
// cannot use enter selection as this pulls data through
sel.selectAll(".y-axis").call(yZoom);
sel.selectAll(".x-axis").call(xZoom);
decorate(sel);
});
let xScaleOriginal = xScale.copy(),
yScaleOriginal = yScale.copy();
let decorate = () => {};
const instance = selection => chart(selection);
// property setters not show
return instance;
};
Here's a pen with the working example:
https://codepen.io/colineberhardt-the-bashful/pen/qBOEEGJ
There are a couple of issues with your code, one which is easy to solve, and one which is not ...
Firstly, the d3-zoom works by storing a transform on the selected DOM element(s) - you can see this via the __zoom property. When the user interacts with the DOM element, this transform is updated and events emitted. Therefore, if you have to different zoom behaviours both of which are controlling the pan / zoom of a single element, you need to keep these transforms synchronised.
You can copy the transform as follows:
selection.call(zoom.transform, d3.event.transform);
However, this will also cause zoom events to be fired from the target behaviour also.
An alternative is to copy directly to the 'stashed' transform property:
selection.node().__zoom = d3.event.transform;
However, there is a bigger problem with what you are trying to achieve. The d3-zoom transform is stored as 3 components of a transformation matrix:
https://github.com/d3/d3-zoom#zoomTransform
As a result, the zoom can only represent a symmetrical scaling together with a translation. Your asymmetrical zoom as a applied to the x-axis cannot be faithfully represented by this transform and re-applied to the plot-area.
This is an upcoming feature, as already noted by #ColinE. The original code is always doing a "temporal zoom" that is un-synced from the transform matrix.
The best workaround is to tweak the xExtent range so that the graph believes that there are additional candles on the sides. This can be achieved by adding pads to the sides. The accessors, instead of being,
[d => d.date]
becomes,
[
() => new Date(taken[0].date.addDays(-xZoom)), // Left pad
d => d.date,
() => new Date(taken[taken.length - 1].date.addDays(xZoom)) // Right pad
]
Sidenote: Note that there is a pad function that should do that but for some reason it works only once and never updates again that's why it is added as an accessors.
Sidenote 2: Function addDays added as a prototype (not the best thing to do) just for simplicity.
Now the zoom event modifies our X zoom factor, xZoom,
zoomFactor = Math.sign(d3.event.sourceEvent.wheelDelta) * -5;
if (zoomFactor) xZoom += zoomFactor;
It is important to read the differential directly from wheelDelta. This is where the unsupported feature is: We can't read from t.x as it will change even if you drag the Y axis.
Finally, recalculate chart.xDomain(xExtent(data.series)); so that the new extent is available.
See the working demo without the jump here: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011
Fixed: Zoom reversing, improved behaviour on trackpad.
Technically you could also tweak yExtent by adding extra d.high and d.low's. Or even both xExtent and yExtent to avoid using the transform matrix at all.
A solution is given here https://observablehq.com/#d3/x-y-zoom
It uses a main zoom behavior that gets the gestures, and two ancillary zooms that store the transforms.

Resources