- Home /
How to curve country meshes based upon a flat projection, into a sphere?
I'm generating country meshes for my grand strategy game by pinging preset positions on a political map image, flood-filling the extents of a country on the political map, and then generating a mesh representation that additionally reflects terrain by getting the greyscale of a black-white heightmap image that has the same proportions and positions like the political map image.
I would like to curve the generated spheres so that all of them form a sphere, in the shape of Earth. I'm sure there is a mathematical formula for what I'm trying to achieve, I'm just so bad at math that I don't even know where to start looking.
The relevant code is:
GameObject seperateLandmass = new GameObject("Landmass" + s);
seperateLandmass.transform.parent = container.transform;
List<GameObject> subMeshes = new List<GameObject>();
seperateLandmass.transform.position = new Vector3(startingCoordinates[s].Item1,0, startingCoordinates[s].Item2);
List<int[]> delimitedCoordinates = seperateCoordinates.Select((x, i) => new { Index = i, Value = x }).GroupBy(x => x.Index / 16383).Select(x => x.Select(v => v.Value).ToArray()).ToList();
for (int i = 0; i < delimitedCoordinates.Count; i++){
vertexIndex = 0;
triangleIndex = 0;
Vector3[] verts = new Vector3[delimitedCoordinates[i].Length * 4];
int[] tris = new int[delimitedCoordinates[i].Length * 6];
for (int j = 0; j < delimitedCoordinates[i].Length; j++){
int x = delimitedCoordinates[i][j] % width;
int y = delimitedCoordinates[i][j] / width;
verts[vertexIndex] = new Vector3(x, heightmap.GetPixel(x, y).grayscale * heightmapMultiplier, y);
verts[vertexIndex+1] = new Vector3(x+1, heightmap.GetPixel(x+1, y).grayscale *heightmapMultiplier,y);
verts[vertexIndex + 2] = new Vector3(x, heightmap.GetPixel(x, y + 1).grayscale * heightmapMultiplier, y + 1);
verts[vertexIndex + 3] = new Vector3(x + 1, heightmap.GetPixel(x + 1, y + 1).grayscale * heightmapMultiplier, y + 1);
tris[triangleIndex] = vertexIndex;
tris[triangleIndex + 1] = vertexIndex + 2;
tris[triangleIndex + 2] = vertexIndex + 1;
tris[triangleIndex + 3] = vertexIndex + 3;
tris[triangleIndex + 4] = vertexIndex + 1;
tris[triangleIndex + 5] = vertexIndex + 2;
vertexIndex += 4;
triangleIndex += 6;
}
Vector2[] uvs = new Vector2[verts.Length];
for (var k = 0; k < uvs.Length; k++) //Give UV coords X,Z world coords
uvs[k] = new Vector2(verts[k].x, verts[k].z);
subMeshes.Add(new GameObject(i.ToString()));
subMeshes[subMeshes.Count - 1].AddComponent<MeshFilter>();
MeshRenderer mr = subMeshes[subMeshes.Count - 1].AddComponent<MeshRenderer>();
Mesh procMesh = new Mesh();
procMesh.vertices = verts.ToArray(); //Assign verts, uvs, and tris to the mesh
procMesh.uv = uvs;
procMesh.triangles = tris.ToArray();
procMesh.RecalculateNormals(); //Determines which way the triangles are facing
The result looks like:
Answer by Vosheck · Jul 07, 2020 at 01:43 PM
The x and y were the pixel coordinates in the equirectangular projection of the world map, so I converted them to longitude and latitude through:
float longitude=2f * Mathf.PI * (float)x / (float)mapWidth - Mathf.PI;
float latitude=return Mathf.PI * ((float)pixelY / (float)mapSize);
Then, the following formula gave me the x,y,z coordinates of vertices arranged in a sphere:
float sinLatitudeRadius = finalRadius * Mathf.Sin(latitude);
new Vector3(sinLatitudeRadius * Mathf.Cos(longitude), finalRadius * Mathf.Cos(latitude), sinLatitudeRadius * Mathf.Sin(longitude));
Additionally, to distort the mesh according to a heightmap, simply multiply the alpha of the heightmap with the final radius in the previous formula.
I hope you realize that that is almost exactly what I wrote. The first two lines are what my Remap function is for and the rest uses the formulas I gave you, just with Sine and Cosine swapped, which is why you had to change the input ranges slightly. The changes you made are mathematically meaningless, both versions essentially do the same thing.
Umm, the latitude turned out to need to be in the range of 0 - Pi, ins$$anonymous$$d of -Pi/2 to Pi/2. Sure, mathematically it is the same range, but functionally they are different values.
Both of the Sine and Cosine formulas result in a sphere being formed, but with a different orientation.
And there was no mention of relief distortion through a radius multiplier.
Thank you for your help. Your answer led me to the path of solving my issue. But, I just felt like a more concise answer that solved my particular problem would be useful here. Ultimately, people are free to read both answers.
Answer by BastianUrbach · Jun 07, 2020 at 11:19 AM
To convert latitude and longitude (which is what you currently seem to use as z and x coordinates) to a point on the unit sphere, you can use these formulas:
x = cos(latitude) * cos(longitude) y = sin(latitude) z = cos(latitude) * sin(longitude)
For this to work, latitude has to be in the range -Pi/2 to Pi/2 and longitude has to be in the range -Pi to Pi.
(That is -90° to 90° and -180° to 180°, converted to radians)
Sine, Cosine and Pi are available in Unity as Mathf.Sin(x), Mathf.Cos(x) and Mathf.PI.
You can remap your coordinates to the ranges given above using Lerp and InverseLerp (or you can do it by hand but this is easier in my opinion):
newValue = Mathf.Lerp(newMin, newMax, Mathf.InverseLerp(oldMin, oldMax, oldValue));
Doesn't quite work. I've seen other videos and answers of the need to remap the coordinates to longitude and latitude, and I've done that successfully.
I don't think that your formula for the sphere's coordinates works, because it makes very distorted meshes. I've implemented the formula from wiki:
x = sin(latitude) * cos(longitude)
y = cos(latitude)
z = sin(latitude) * sin(longitude)
and while it does draw a country's mesh well, it doesn't curve it along a sphere at all, it just angles it differently than it previously was.
No the formulas definitely work. Here is a simple usage example, put that script on Unity's default plane object, hit Play and you'll get a sphere as expected:
using UnityEngine;
using static UnityEngine.$$anonymous$$athf;
public class $$anonymous$$akeSpherical : $$anonymous$$onoBehaviour {
float Remap(float value, float old$$anonymous$$in, float old$$anonymous$$ax, float new$$anonymous$$in, float new$$anonymous$$ax) {
return Lerp(new$$anonymous$$in, new$$anonymous$$ax, InverseLerp(old$$anonymous$$in, old$$anonymous$$ax, value));
}
void Start() {
var mesh = GetComponent<$$anonymous$$eshFilter>().mesh;
var vertices = mesh.vertices;
var old$$anonymous$$in = mesh.bounds.$$anonymous$$;
var old$$anonymous$$ax = mesh.bounds.max;
for (var i = 0; i < vertices.Length; i++) {
var v = vertices[i];
var lon = Remap(v.x, old$$anonymous$$in.x, old$$anonymous$$ax.x, PI, -PI);
var lat = Remap(v.y, old$$anonymous$$in.y, old$$anonymous$$ax.y, -PI / 2, PI / 2);
vertices[i] = new Vector3(Cos(lon) * Cos(lat), Sin(lat), Sin(lon) * Cos(lat));
}
mesh.vertices = vertices;
mesh.RecalculateBounds();
mesh.RecalculateNormals();
GetComponent<$$anonymous$$eshFilter>().mesh = mesh;
}
}
I tried your script with a primitive cube and I don't see a way how the -1 and 1 variance of the y is supposed to draw a sphere.
Those are the correct values, it's just that a standard cube mesh is exceptionally badly suited for this. First of all, you can't build anything remotely spherical from the 12 triangles of a standard cube mesh. Second, half of the vertices of a cube are at $$anonymous$$ z, which is mapped to the south pole (0, -1, 0) and the other half is at max z, which is mapped to the north pole (0, +1, 0). Think of a world map and how the entire top edge of the map "shrinks" into the north pole when you map it to a globe. That's exactly what's happening here. Try it with a subdivided cube with a lot more vertices and you'll get something that looks a lot more spherical. That being said, the script doesn't do magic, it expects the mesh to be somewhat reasonable for the given task. Perhaps I misunderstood you but the way I understood it is that you have a fairly high resolution mesh of the world that's flat in the XZ-Plane and should be mapped to a sphere ins$$anonymous$$d. That is what the script does (it's just an example for how to use the formulas though, you might have to adapt it a little to your specific usecase).
Your answer
Follow this Question
Related Questions
How do I make an interactive map on Unity? 0 Answers
Assigning UV Map to model at runtime 0 Answers
Quality Reduction only reduces my player sprite quality 0 Answers
What is Mip Maps (Pictures) 2 Answers