-
Notifications
You must be signed in to change notification settings - Fork 9
Description
The current implementation of the irregular grid can look quite terrible, depending on the provided data. This has been discussed in the PR #33
It was implemented as a point cloud with all points having the exact same size regardless of their position on the globe and a very simplistic approach to change their size while zooming the camera. This has to be done manually. In order to do so properly, several things need to be taken into account:
- Camera-Distance: The closer the camera, the bigger the points
- Size-Attenuation: Since the globe is a globe, the points at the edge of the viewpoint have to appear smaller than the ones in the center
- density: In datasets with lower resolution or irregular grids with changing point numbers across the globe, areas with fewer points may have larger points than areas with higher density. The package
Delaunatorlooks promising to calculate the density for each single point in an acceptable period of time, however has the weakness that it creates a seam, at the edge of the longitudes (usually where 0 and 360 meet), as it is not built for our usecase.
This could like here for the vertex shader (keep in mind, that the math here is completely bonkers and made by ChatGPT, this is just an example how to apply the density onto the globe):
attribute float data_value;
attribute float localDensity;
uniform float baseSize;
varying float vDensity;
varying float v_value;
void main() {
v_value = data_value;
vDensity = localDensity;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mvPosition;
// Compensate for perspective scaling
float size = baseSize / (vDensity * 5.0 + 1.0);
size = size * (10.0 / -mvPosition.z); // Adjust constant (300.0) for your scene scaling
gl_PointSize = size;
}
And for the GlobeIrregular-Code, this could be something like this here
function haversineDistance(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371; // Earth's radius in km
const toRad = Math.PI / 180;
const dLat = (lat2 - lat1) * toRad;
const dLon = (lon2 - lon1) * toRad;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * toRad) * Math.cos(lat2 * toRad) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function estimateLocalDensityLatLon(latLons: [number, number][]): Float32Array {
const N = latLons.length;
const delaunay = Delaunator.from(latLons);
const { triangles } = delaunay;
// Prepare adjacency lists
const adjacency: number[][] = Array.from({ length: N }, () => []);
const densities = new Float32Array(N);
for (let i = 0; i < triangles.length; i += 3) {
const a = triangles[i];
const b = triangles[i + 1];
const c = triangles[i + 2];
// Add undirected edges
adjacency[a].push(b, c);
adjacency[b].push(a, c);
adjacency[c].push(a, b);
}
// Remove duplicates from adjacency lists
for (let i = 0; i < N; i++) {
adjacency[i] = Array.from(new Set(adjacency[i]));
}
// Estimate local density based on average neighbor distance
for (let i = 0; i < N; i++) {
const [lat1, lon1] = latLons[i];
const neighbors = adjacency[i];
if (neighbors.length === 0) {
densities[i] = 0; // Isolated point
continue;
}
let sumDistances = 0;
for (const j of neighbors) {
const [lat2, lon2] = latLons[j];
sumDistances += haversineDistance(lat1, lon1, lat2, lon2);
}
const avgDistance = sumDistances / neighbors.length;
// Density can be inversely proportional to distance
densities[i] = 1 / avgDistance;
}
let min = Infinity;
let max = -Infinity;
for (let i = 0; i < densities.length; i++) {
if (densities[i] < min) min = densities[i];
if (densities[i] > max) max = densities[i];
}
// Normalize densities to [0, 1]
for (let i = 0; i < densities.length; i++) {
densities[i] = (densities[i] - min) / (max - min);
}
return densities;
}
...
// in getGrid
const points2D: [number, number][] = [];
for (let i = 0; i < N; i++) {
points2D.push([longitudes[i], latitudes[i]]);
}
const densities = estimateLocalDensityLatLon(points2D);
points!.geometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
points!.geometry.setAttribute(
"localDensity",
new THREE.BufferAttribute(densities, 1)
);
Another thing is the question if we want to apply a fade-out to the points, to come closer to gaussian splatting, this can be implemented quite easily in the fragmentShader via
float falloff = exp(-r2 * 2.0); // Adjust the 4.0 as needed (sharpness)
gl_FragColor = vec4(color, falloff);
or something like this. A working example is available in the aforementioned PR in the txt-files. The used Material should enable some kind of blending as well, like blending: THREE.AdditiveBlending.