Skip to content

Commit

Permalink
Merge pull request #75 from cmurphy23/spherical_projection
Browse files Browse the repository at this point in the history
Project Web Mercator Tiles to Sphere with ShaderMaterial
  • Loading branch information
tentone authored Jul 15, 2024
2 parents f846323 + 31dd419 commit 259e87f
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 25 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ pnpm-lock.yaml
yarn.lock

examples/assets/

.history/
34 changes: 19 additions & 15 deletions source/nodes/MapNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,21 +259,7 @@ export abstract class MapNode extends Mesh
try
{
const image: HTMLImageElement = await this.mapView.provider.fetchTile(this.level, this.x, this.y);

if (this.disposed)
{
return;
}

const texture = new Texture(image);
texture.generateMipmaps = false;
texture.format = RGBAFormat;
texture.magFilter = LinearFilter;
texture.minFilter = LinearFilter;
texture.needsUpdate = true;

// @ts-ignore
this.material.map = texture;
await this.applyTexture(image);
}
catch (e)
{
Expand All @@ -292,6 +278,24 @@ export abstract class MapNode extends Mesh
this.material.needsUpdate = true;
}

public async applyTexture(image: HTMLImageElement): Promise<void>
{
if (this.disposed)
{
return;
}

const texture = new Texture(image);
texture.generateMipmaps = false;
texture.format = RGBAFormat;
texture.magFilter = LinearFilter;
texture.minFilter = LinearFilter;
texture.needsUpdate = true;

// @ts-ignore
this.material.map = texture;
}

/**
* Increment the child loaded counter.
*
Expand Down
73 changes: 65 additions & 8 deletions source/nodes/MapSphereNode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Matrix4, BufferGeometry, MeshBasicMaterial, Quaternion, Vector3, Raycaster, Intersection} from 'three';
import {Matrix4, BufferGeometry, Quaternion, Vector3, Raycaster, Intersection, ShaderMaterial, TextureLoader, Texture, Vector4} from 'three';
import {MapNode, QuadTreePosition} from './MapNode';
import {MapSphereNodeGeometry} from '../geometries/MapSphereNodeGeometry';
import {UnitsUtils} from '../utils/UnitsUtils';
Expand Down Expand Up @@ -35,7 +35,50 @@ export class MapSphereNode extends MapNode

public constructor(parentNode = null, mapView = null, location = QuadTreePosition.root, level = 0, x = 0, y = 0)
{
super(parentNode, mapView, location, level, x, y, MapSphereNode.createGeometry(level, x, y), new MeshBasicMaterial({wireframe: false}));
let bounds = UnitsUtils.tileBounds(level, x, y);

// Load shaders
const vertexShader = `
varying vec3 vPosition;
void main() {
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const fragmentShader = `
#define PI 3.1415926538
varying vec3 vPosition;
uniform sampler2D uTexture;
uniform vec4 webMercatorBounds;
void main() {
// this could also be a constant, but for some reason using a constant causes more visible tile gaps at high zoom
float radius = length(vPosition);
float latitude = asin(vPosition.y / radius);
float longitude = atan(-vPosition.z, vPosition.x);
float web_mercator_x = radius * longitude;
float web_mercator_y = radius * log(tan(PI / 4.0 + latitude / 2.0));
float y = (web_mercator_y - webMercatorBounds.z) / webMercatorBounds.w;
float x = (web_mercator_x - webMercatorBounds.x) / webMercatorBounds.y;
vec4 color = texture2D(uTexture, vec2(x, y));
gl_FragColor = color;
}
`;

// Create shader material
let vBounds = new Vector4(...bounds);
const material = new ShaderMaterial({
uniforms: {uTexture: {value: new Texture()}, webMercatorBounds: {value: vBounds}},
vertexShader: vertexShader,
fragmentShader: fragmentShader
});

super(parentNode, mapView, location, level, x, y, MapSphereNode.createGeometry(level, x, y), material);

this.applyScaleNode();

Expand Down Expand Up @@ -67,16 +110,30 @@ export class MapSphereNode extends MapNode
const segments = Math.floor(MapSphereNode.segments * (max / (zoom + 1)) / max);

// X
const phiLength = 1 / range * 2 * Math.PI;
const phiStart = x * phiLength;

const lon1 = x > 0 ? UnitsUtils.webMercatorToLongitude(zoom, x) + Math.PI : 0;
const lon2 = x < range - 1 ? UnitsUtils.webMercatorToLongitude(zoom, x+1) + Math.PI : 2 * Math.PI;
const phiStart = lon1;
const phiLength = lon2 - lon1;

// Y
const thetaLength = 1 / range * Math.PI;
const thetaStart = y * thetaLength;

const lat1 = y > 0 ? UnitsUtils.webMercatorToLatitude(zoom, y) : Math.PI / 2;
const lat2 = y < range - 1 ? UnitsUtils.webMercatorToLatitude(zoom, y+1) : -Math.PI / 2;
const thetaLength = lat1 - lat2;
const thetaStart = Math.PI - (lat1 + Math.PI / 2);

return new MapSphereNodeGeometry(1, segments, segments, phiStart, phiLength, thetaStart, thetaLength);
}

public async applyTexture(image: HTMLImageElement): Promise<void>
{
const textureLoader = new TextureLoader();
const texture = textureLoader.load(image.src, function() {});
// @ts-ignore
this.material.uniforms.uTexture.value = texture;
// @ts-ignore
this.material.uniforms.uTexture.needsUpdate = true;
}

/**
* Apply scale and offset position to the sphere node geometry.
*/
Expand Down
64 changes: 62 additions & 2 deletions source/utils/UnitsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ export class UnitsUtils
public static EARTH_ORIGIN: number = UnitsUtils.EARTH_PERIMETER / 2.0;

/**
* Converts coordinates from WGS84 Datum to XY in Spherical Mercator EPSG:900913.
* Largest web mercator coordinate value, both X and Y range from negative extent to positive extent
*/
public static WEB_MERCATOR_MAX_EXTENT: number = 20037508.34;

/**
* Converts coordinates from WGS84 Datum to XY in Spherical Web Mercator EPSG:900913.
*
* @param latitude - Latitude value in degrees.
* @param longitude - Longitude value in degrees.
Expand All @@ -54,7 +59,7 @@ export class UnitsUtils
}

/**
* Converts XY point from Spherical Mercator EPSG:900913 to WGS84 Datum.
* Converts XY point from Spherical Web Mercator EPSG:900913 to WGS84 Datum.
*
* @param x - X coordinate.
* @param y - Y coordinate.
Expand Down Expand Up @@ -140,4 +145,59 @@ export class UnitsUtils
{
return (color.r * 255.0 * 65536.0 + color.g * 255.0 * 256.0 + color.b * 255.0) * 0.1 - 10000.0;
}

/**
* Get the size of a tile in web mercator coordinates
* *
* @param zoom - the zoom level of the tile
* @returns the size of the tile in web mercator coordinates
*/
public static getTileSize(zoom: number): number
{
const maxExtent = UnitsUtils.WEB_MERCATOR_MAX_EXTENT;
const numTiles = Math.pow(2, zoom);
return 2 * maxExtent / numTiles;
}

/**
* Get the bounds of a tile in web mercator coordinates
* *
* @param zoom - the zoom level of the tile
* @param x - the x coordinate of the tile
* @param y - the y coordinate of the tile
* @returns list of bounds - [startX, sizeX, startY, sizeY]
*/
public static tileBounds(zoom: number, x: number, y: number): number[]
{
const tileSize = UnitsUtils.getTileSize(zoom);
const minX = -UnitsUtils.WEB_MERCATOR_MAX_EXTENT + x * tileSize;
const minY = UnitsUtils.WEB_MERCATOR_MAX_EXTENT - (y + 1) * tileSize;
return [minX, tileSize, minY, tileSize];
}

/**
* Get the latitude value of a given web mercator coordinate and zoom level
*
* @param zoom - the zoom level of the coordinate
* @param y - the y web mercator coordinate
* @returns - latitude of coordinate in radians
*/
public static webMercatorToLatitude(zoom: number, y: number): number
{
const yMerc = UnitsUtils.WEB_MERCATOR_MAX_EXTENT - y * UnitsUtils.getTileSize(zoom);
return Math.atan(Math.sinh(yMerc / UnitsUtils.EARTH_RADIUS));
}

/**
* Get the latitude value of a given web mercator coordinate and zoom level
*
* @param zoom - the zoom level of the coordinate
* @param x - the x web mercator coordinate
* @returns - longitude of coordinate in radians
*/
public static webMercatorToLongitude(zoom: number, x: number): number
{
const xMerc = -UnitsUtils.WEB_MERCATOR_MAX_EXTENT + x * UnitsUtils.getTileSize(zoom);
return xMerc / UnitsUtils.EARTH_RADIUS;
}
}

0 comments on commit 259e87f

Please sign in to comment.