Skip to content

Irregular Grid - Experiments and thoughts #42

@Karinon

Description

@Karinon

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 Delaunator looks 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions