Skip to content
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

Project Web Mercator Tiles to Sphere with ShaderMaterial #75

Merged
merged 4 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}