Scenario
I'm building an interactive using 2d tiles from Open Street Maps. I have a shader that wraps tiles around a sphere but the tiles appear too tall near the poles and too short near the equator.
tiles near the equator are too short, making content look stretched horizontally
tiles near the north pole are too tall, making content look stretched vertically
My initial thought was to use Mathf.Sin
to shorten tiles near the poles (sin0) while expanding tiles near the equator (sin1). In theory the idea seems sound but in practice it's not going so well.
Question
How can I get my shader to adjust for necessary 2d-to-3d vertical tile adjustments ? I've been looking for some condensed shader code that takes care of everything in it's v2f function but I haven't had much luck.
some code
BaseTile->GetThetaPhiFromXY (core of what GetThetaPhiFromKey uses)
public static Vector2
GetThetaPhiFromXY(float x, float y, int zoomLevel, bool stretch = false)
{
Vector2 tilesXY = TileGroup.GetTilesXY(zoomLevel);
float theta = (x + 0.5f) / tilesXY.x * Mathf.PI * 2 - Mathf.PI / 2;
float phi = (y + 0.5f) / tilesXY.y * Mathf.PI;
// // stretch tiles vertically (poles are shorter, equator is taller) @jkr
// if (stretch == true)
// {
// float PI2 = Mathf.PI / 2;
// float sin = Mathf.Sin(phi);
// float phiSin = phi * sin;
// if (
// y > tilesXY.y / 2 - 1 // tiles north of equator @jkr
// )
// {
// // phi range 0-PI:bot2top
// // sin range 0-1:bot2mid, 1-0:mid2top
// float neuePhi = phi - PI2;
// phiSin = neuePhi * sin;
// // phiSin += PI2;
// phiSin = Mathf.PI;
// }
// Debug.Log($"{y} {phi} {sin}");
// phi = phiSin;
// }
return new Vector2(theta, phi);
}
TileBender.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/**
* this class helps with prepping the tiles for the TileBender shader
* @jkr
*/
public class TileBender : MonoBehaviour
{
void Start()
{
// SetUpTileLatLons(GetComponent<MeshFilter>().mesh, new TileKey(0, 0, 0));
}
private Vector2 GetThetaPhiOfVertex(TileKey tileKey, Vector2 uv)
{
// if (tileKey.x != 0) return Vector2.zero;
TileKey tileKey2 = tileKey;
tileKey2.x += 1;
tileKey2.y += 1;
Vector3 startPos = BaseTile.GetThetaPhiFromKey(tileKey, true);
Vector3 endPos = BaseTile.GetThetaPhiFromKey(tileKey2, true);
// todo: this can be done faster, but how ? @jkr
Vector3 diff2 = (endPos - startPos) / 2;
startPos -= diff2;
endPos -= diff2;
float theta = Mathf.Lerp(startPos.x, endPos.x, uv.x);
float phi = Mathf.Lerp(startPos.y, endPos.y, uv.y);
// Debug.Log($"{startPos.x}, {endPos.x}");
// Debug.Log($"{startPos.x} -- {endPos.x} -- {uv.x} -- {theta}");
return new Vector2(theta, phi);
}
public void SetUpTilePairs(Mesh mesh, TileKey tileKey)
{
Vector2[] uvs = mesh.uv;
Vector2[] thetaPhiPairs = new Vector2[uvs.Length];
for (int i = 0; i < thetaPhiPairs.Length; ++i)
{
thetaPhiPairs[i] = GetThetaPhiOfVertex(tileKey, uvs[i]);
}
mesh.uv2 = thetaPhiPairs;
}
}
TileBender.shader
Shader "Custom/TileBender" {
Properties{
_MainTex("Tex", 2D) = "" {}
_SphereCenter("SphereCenter", Vector) = (0, 0, 0, 1)
_EarthRadius("EarthRadius", Float) = 5
_ColorAlpha("TextureAlpha", Float) = 0.5
}
SubShader{
// Cull off // for doublesized texture @jkr todo: disable for prod
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
ZWrite Off
// ZTest Off
Blend SrcAlpha OneMinusSrcAlpha
// Cull front
// LOD 100
Pass {
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float2 uv : TEXCOORD0;
float2 thetaPhi : TEXCOORD1;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 norm : NORMAL;
float2 uv : TEXCOORD0;
};
uniform float _EarthRadius;
float4 _SphereCenter;
v2f vert(appdata v)
{
v2f o;
float theta = v.thetaPhi.x;
float phi = v.thetaPhi.y;
float4 posOffsetWorld = float4(
_EarthRadius*sin(phi)*cos(theta),
_EarthRadius*-cos(phi),
_EarthRadius*sin(phi)*sin(theta),
0);
float4 posObj = mul(unity_WorldToObject,
posOffsetWorld + _SphereCenter);
o.pos = UnityObjectToClipPos(posObj);
o.uv = v.uv;
o.norm = mul(unity_WorldToObject, posOffsetWorld);
return o;
}
uniform fixed _TextureAlpha = 0.5;
sampler2D _MainTex;
float4 frag(v2f IN) : COLOR
{
fixed4 col = tex2D(_MainTex, IN.uv);
col[3] = _TextureAlpha; // transparency
return col;
}
ENDHLSL
}
}
FallBack "VertexLit"
}
** EDIT **
Based on @derHugo's comment I've started breaking down a formula from this wikipedia page about Web Mercator projection. I've only just started converting it to c# and here's the work-in-progress so far. I'll modify the edit as it progresses.
private void PointConversions() {
Vector3 pos = this.Camera.transform.position;
Vector3 rot = this.Camera.transform.rotation.eulerAngles;
// Debug.Log($"{rot.x} -- {rot.y}");
float lambda = rot.y; // longitude
if(pos.x > 0) lambda = (0 - (lambda - 360)) * -1;
lambda = lambda / 360 * PI;
float phi = rot.x; // latitude
if(pos.y < 0) phi = (0 - (phi - 360) * -1);
phi = phi / 360 * PI;
// Debug.Log($"{lambda} -- {phi}");
Vector2 lonLat = new Vector2(lambda, phi);
Vector2 pt3d = Point2DFromLonLat(lonLat);
Debug.Log(pt3d);
}
private Vector2 Point2DFromLonLat(Vector2 lonLat) {
float lon = lonLat.x;
float lat = lonLat.y;
float x = size / PI2;
x *= Mathf.Pow(zoomLevel, 2);
x *= lon + PI;
float y = size / PI2;
y *= Mathf.Pow(zoomLevel, 2);
float subY = Mathf.Tan((PI/4) + (lat/2));
y *= PI - Mathf.Log(subY);
// Debug.Log($"{x} -- {y}");
Vector2 point = Vector3.zero;
point.x = x;
point.y = y;
// point.z = earthRadius;
return point;
}
private Vector2 Point3dTo2D(Vector3 xyz) {
return Vector2.zero;
}
** Latest Solution **
I am pretty sure that I've overthought this entire project. That said I've stripped things down to do find a major pain point which is finding an appropriate way to convert mercator tiles to equirectangular tiles on the fly.
the "magic" formula is the closest I've come to a Gudermannian Inverse
float phi = (float)Math.Atan(Math.Sinh(latRad *2));
It's not perfect because the original formula found does not require *2
so something else in my app must be wrong