Skepticism on Unity. Flat Earth? how do you morph Unity terrain into sphere?

This is insane. Has anyone seen Google Earth & Google Map? When you zoom out you get nice earth sphere. But Unity 3D. it’s glass dome and terrain. Unable to rotate terrain any angle and place around the sphere. Terrain can only be move around circle.

On unity. What’s source code for morph terrain into sphere?

It’s not a good idea to use Unity terrain for this purpose. In fact it just isn’t compatible. You can butcher it into doing something but it will always be broken.

Instead consider using a mesh.

4 Likes

Alternatively, and humorously this goes somewhat in line with the message the video was suggesting, you could use an effect that gives the observer (the player) the impression that they are on a round world when it isn’t round in the slightest. Below is a very well rated asset that can create a curved world visual effect while leaving the scene unaffected.

5 Likes

Holy crap, that is impressive

That’s nice indeed but you will have a lot of problems with Unity terrain even with it still, but yeah probably best option for OP!

When approaching planet from space, create dummy terrain patch object (dense grid with or without hardware subdivision), roughly 10x10 km, and use its center as world origin for purposes of rendering and origin shifting. For vertices in the fragment, adjust positions and altitudes based on map coordinates and elevation data.

When approaching boundary of the patch, move patch to a new position, rebuild based on elevation data from new location, and origin shift to center of the new patch…

Should be fairly straightforward.

See, this is why we need to move away from square terrain maps and switch to triangles. That way they can be stitched together to make a icosahedron based globe.

Left sphere: BOOOOOO!!!

Right sphere: YAAAAAY!!!

EDIT: Also, the terrain should be made into part of a globe by default so that the horizon shows the curvature of the planet.

The challenge…placing hand made terrains on the sphere. How do you translate from rectangular terrain heightmaps to the sphere?
I’m not talking procedural, but hand made. Using something like World Creator, Gaia, TC2, etc.

And before you say a QuadSphere…uh uh. The corners will get you.

You will still have to deal with map coordinates. Also, isntead of icosahedron, I think a much better idea would be to sue quad based sphere, because mapping coordinates to polygons should be much more straightforward.

3091673--233248--cube-sphere.png

Also… having round planet by default will introduce complication in physics (because gravity is now directed towards specific point and not in the same direction for everybody), plus with default floating point precision planet will be going beyond the range where precision errors start appearing.

Now, triangle-based sphere does have a few advantages - unity does not support quad based domains in hardware tesselator, so triangle-based mesh will be slightly more suitable for hardware subdivision.

2 Likes

Just to get this out of the way, comparing Google Maps/Earth with a game engine is asinine.

When most of us think of interacting with the environment around us, we don’t think about the world as a sphere. Down is just ‘down’, not a point 4,000 miles below our feet. And for most practical applications, a flat terrain serves this purpose perfectly.

Saying the default should be changed to suit a very particular use case is not a great idea. I could support adding another “PlanetaryTerrain” option, but not changing the default.

6 Likes

Yes, because then we’d have to cave in to a certain someone’s desire for 64-bit floats. :stuck_out_tongue:

3 Likes

64bit floats are so last century. We need 128bit float. This way we’ll be able to address any point in the observable universe with micron precision. Think about the possibilities!

(That was a joke).

Binary128 has 112 fractonal bits, which allows to store values up to

2^112 micros is according to wolfram alpha this is bigger than observable universe by factor of 5 or 6)

5 Likes

A quad sphere is great for outer space. But the problem is the transition to planet surface.
If you try to have a terrain per face, then as long as you are in the middle or middle edge of the terrain, it’s fine. You can pull in tiles around the square you are in, so 3 above your square, 3 below, one to the left and one to the right. 9 tiles in all, including the one you are in.

By tiles, image each subdivision face you show on the quadsphere as being in a terrain section to show around the player.

But the corners mean you can’t pull in from nice tiles of terrain around you, you don’t have a nice grid-based layout of tiles to pull from. There is some serious warping going on there.

I’m talking about a scenario where you have a large terrain for each face, that you have to split into tiles. I guess you could do some sort of math, and warp the faces around the player to appear flat. But that would mean in loading in a lot of data, and showing some pretty big terrains.

That’s right, at some level you’d have to be working with some spherical coordinate system or another and map that to the terrain data, and that mapping is in fact going to have to account for some amount of warp on all terrain tiles.

Yeah. Honestly…I’m almost resigned to just doing cylindrical mapping of a terrain onto a grid. The poles will look pinched from orbit, and won’t really match the poles when you are on the surface…meh. I don’t think it’s a big enough issue, and I’m not smart enough it seems to figure out a better way that doesn’t involve some sort of math degree.

I don’t see any problem.

You’ll need to draw map for the player using any proejction of your choice, and within one of the planetary quads (x+,. x-, y+, y-, z+, z-) underlying quads can be made to be of uniform size. Basically you’ll have 6 connected gird maps per planet with nice tile-based layout. It can’t be made cleaner/simpler than this.

1 Like

I did a few tests… it is WAAAAY easier to make a quad-based sphere than dealing with anything triangular.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class PlanetGenerator: MonoBehaviour{
    [SerializeField] int sphereGridSize = 6;
    [SerializeField] float radius = 1.0f;
    [SerializeField] int numTetraSubdivisions = 3;
    [SerializeField] bool showSphereGrid = true;
    [SerializeField] bool showTetraGrid = true;

    GameObject tetra = null;
    GameObject sphereGrid = null;
    Mesh sphereMesh = null;
    Mesh tetraMesh = null;
    MeshRenderer sphereRenderer = null;
    MeshRenderer tetraRenderer = null;
    MeshFilter sphereFilter = null;
    MeshFilter tetraFilter = null;
    [SerializeField] Material meshMaterial = null;

    List<Vector3> verts = new List<Vector3>();
    List<int> indexes = new List<int>();
    List<Vector3> normals = new List<Vector3>();

    float lastRadius = -1.0f;
    int lastGridSize = -1;
    int lastSubdivisions = -1;

    void killObjects(){
        DestroyImmediate(tetraFilter);
        DestroyImmediate(sphereFilter);
        DestroyImmediate(tetraRenderer);
        DestroyImmediate(sphereRenderer);
        DestroyImmediate(sphereMesh);
        DestroyImmediate(tetraMesh);
        DestroyImmediate(tetra);
        DestroyImmediate(sphereGrid);
    }

    C getOrCreateTmpComponent<C>(GameObject obj) where C: Component{
        var result = obj.GetComponent<C>();
        if (result)
            return result;
        result = obj.AddComponent<C>();
        result.hideFlags = HideFlags.DontSave;
        return result;
    }
    void rebuildMeshes(){
        if (!tetra){
            tetra = new GameObject();
            tetra.transform.SetParent (transform);
            tetra.transform.localPosition = new Vector3(radius, 0.0f, 0.0f);
            tetra.hideFlags = HideFlags.DontSave;
        }
        if (!sphereGrid){
            sphereGrid = new GameObject();
            sphereGrid.transform.SetParent (transform);
            sphereGrid.transform.localPosition = new Vector3(-radius, 0.0f, 0.0f);
            sphereGrid.hideFlags = HideFlags.DontSave;
        }
        sphereFilter = getOrCreateTmpComponent<MeshFilter>(sphereGrid);
        tetraFilter = getOrCreateTmpComponent<MeshFilter>(tetra);
        sphereRenderer = getOrCreateTmpComponent<MeshRenderer>(sphereGrid);
        tetraRenderer = getOrCreateTmpComponent<MeshRenderer>(tetra);
        if (!sphereMesh){
            sphereMesh = new Mesh();
            sphereMesh.hideFlags = HideFlags.DontSave;
            sphereFilter.mesh = sphereMesh;
        }
        if (!tetraMesh){
            tetraMesh = new Mesh();
            tetraMesh.hideFlags = HideFlags.DontSave;
            tetraFilter.mesh = tetraMesh;
        }
        sphereRenderer.sharedMaterial = meshMaterial;
        tetraRenderer.sharedMaterial = meshMaterial;

        verts.Clear();
        normals.Clear();
        indexes.Clear();

        processTetraGrid(numTetraSubdivisions, (a, b, c)=>{
            var idx = verts.Count;
            var n = Vector3.Cross(b - a, c -a).normalized;
            verts.Add(a * radius);
            verts.Add(b * radius);
            verts.Add(c * radius);
            normals.Add(n);
            normals.Add(n);
            normals.Add(n);
            indexes.Add(idx + 0);
            indexes.Add(idx + 1);
            indexes.Add(idx + 2);
        });
        tetraMesh.Clear();
        tetraMesh.subMeshCount = 1;
        tetraMesh.SetVertices(verts);
        tetraMesh.SetNormals(normals);
        tetraMesh.SetTriangles(indexes, 0);
        tetraRenderer.sharedMaterial = meshMaterial;

        verts.Clear();
        indexes.Clear();
        normals.Clear();

        processSphereGrid(sphereGridSize, (a, b, c, d) => {
            var idx = verts.Count;
            var n = Vector3.Cross(c - a, b - a).normalized;
            verts.Add(a * radius);
            verts.Add(b * radius);
            verts.Add(c * radius);
            verts.Add(d * radius);

            normals.Add(n);
            normals.Add(n);
            normals.Add(n);
            normals.Add(n);

            indexes.Add(idx + 0);
            indexes.Add(idx + 2);
            indexes.Add(idx + 1);
            indexes.Add(idx + 1);
            indexes.Add(idx + 2);
            indexes.Add(idx + 3);
        });
        sphereMesh.Clear();
        sphereMesh.subMeshCount = 1;
        sphereMesh.SetVertices(verts);
        sphereMesh.SetNormals(normals);
        sphereMesh.SetTriangles(indexes, 0);
        sphereRenderer.sharedMaterial = meshMaterial;
    }

    static readonly float sq3 = Mathf.Sqrt(1.0f/3);

    static Vector3 getPoint(float u, float v, Vector3 xVec, Vector3 yVec, Vector3 zVec, Vector3 posVec){
        var xMinTop = new Vector3(-sq3, sq3, sq3);
        var xMinBottom = new Vector3(-sq3, -sq3, sq3);
        var xMaxTop = new Vector3(sq3, sq3, sq3);
        var xMaxBottom = new Vector3(sq3, -sq3, sq3);

        //slerp
        var xMin = Vector3.Slerp(xMinTop, xMinBottom, v);
        var xMax = Vector3.Slerp(xMaxTop, xMaxBottom, v);
        var result = Vector3.Slerp(xMin, xMax, u);
        /*
        //lerp
        var xMin = Vector3.Lerp(xMinTop, xMinBottom, v);
        var xMax = Vector3.Lerp(xMaxTop, xMaxBottom, v);
        var result = Vector3.Lerp(xMin, xMax, u).normalized;
        */
        return posVec + result.x * xVec + result.y * yVec + result.z * zVec;
    }

    static void processFaceGrid(Vector3 xVec, Vector3 yVec, Vector3 zVec, int gridSize, QuadCallback callback){
        float valStep = 1.0f/gridSize;//Mathf.PI/(2.0f*gridSize);

        float valStart = 0.0f;//-Mathf.PI*0.25f;
        for(int x = 0; x < gridSize; x++){
            float u1 = valStart + valStep * x;
            float u2 = valStart + valStep * (x + 1);
            for (int y = 0; y < gridSize; y++){
                float v1 = valStart + valStep * y;
                float v2 = valStart + valStep * (y + 1);

                var p1 = getPoint(u1, v1, xVec, yVec, zVec, Vector3.zero);
                var p2 = getPoint(u2, v1, xVec, yVec, zVec, Vector3.zero);
                var p3 = getPoint(u1, v2, xVec, yVec, zVec, Vector3.zero);
                var p4 = getPoint(u2, v2, xVec, yVec, zVec, Vector3.zero);

                callback(p1, p2, p3, p4);
                /*Gizmos.DrawLine(p1, p2);
                Gizmos.DrawLine(p3, p4);
                Gizmos.DrawLine(p1, p3);
                Gizmos.DrawLine(p2, p4);*/
            }
        }
    }

    static void processSphereGrid(int gridSize, QuadCallback cb){
        processFaceGrid(Vector3.right, Vector3.up, Vector3.forward, gridSize, cb);
        processFaceGrid(-Vector3.right, Vector3.up, -Vector3.forward, gridSize, cb);

        processFaceGrid(Vector3.forward, Vector3.up, -Vector3.right, gridSize, cb);
        processFaceGrid(-Vector3.forward, Vector3.up, Vector3.right, gridSize, cb);

        processFaceGrid(Vector3.right, -Vector3.forward, Vector3.up, gridSize, cb);
        processFaceGrid(-Vector3.right, -Vector3.forward, -Vector3.up, gridSize, cb);
    }

    void drawSphereGrid(){
        if (!showSphereGrid)
            return;
        Gizmos.color = Color.green;

        var offset = transform.up * radius * 2.0f - transform.right * radius;
        processSphereGrid(sphereGridSize, (a, b, c, d) => {
            var a1 = transform.TransformPoint(a * radius) + offset;
            var b1 = transform.TransformPoint(b * radius) + offset;
            var c1 = transform.TransformPoint(c * radius) + offset;
            var d1 = transform.TransformPoint(d * radius) + offset;
            Gizmos.DrawLine(a1, b1);
            Gizmos.DrawLine(b1, c1);
            Gizmos.DrawLine(c1, d1);
            Gizmos.DrawLine(d1, a1);
        });
    }

    static void processSphereTriangle(Vector3 a, Vector3 b, Vector3 c, int numSubdivisions, TriangleCallback callback){
        if (numSubdivisions <= 1){
            callback(a, b, c);
            return;
        }
        else{
            var ab = ((a + b) * 0.5f).normalized;
            var ac = ((a + c) * 0.5f).normalized;
            var bc = ((b + c) * 0.5f).normalized;

            var nextSubdiv = numSubdivisions-1;
            processSphereTriangle(a, ab, ac, nextSubdiv, callback);
            processSphereTriangle(ab, b, bc, nextSubdiv, callback);
            processSphereTriangle(bc, c, ac, nextSubdiv, callback);
            processSphereTriangle(ab, bc, ac, nextSubdiv, callback);
        }

    }

    public delegate void TriangleCallback(Vector3 a, Vector3 b, Vector3 c);
    public delegate void QuadCallback(Vector3 a, Vector3 b, Vector3 c, Vector3 d);

    static void processTetraGrid(int numSubdivs, TriangleCallback callback){
        var f2 = 1.0f/Mathf.Sqrt(2.0f);
        var a = new Vector3(1.0f, 0.0f, -f2);
        var b = new Vector3(-1.0f, 0.0f, -f2);
        var c = new Vector3(0.0f, 1.0f, f2);
        var d = new Vector3(0.0f, -1.0f, f2);

        a = a.normalized;
        b = b.normalized;
        c = c.normalized;
        d = d.normalized;

        processSphereTriangle(a, b, c, numSubdivs, callback);
        processSphereTriangle(a, d, b, numSubdivs, callback);
        processSphereTriangle(a, c, d, numSubdivs, callback);
        processSphereTriangle(b, d, c, numSubdivs, callback);
    }

    void drawTetraGrid(){
        if (!showTetraGrid)
            return;

        var offset = transform.up * radius * 2.0f + transform.right * radius;

        Gizmos.color = Color.yellow;

        processTetraGrid(numTetraSubdivisions, (a, b, c) =>{
            var a1 = transform.TransformPoint(a * radius) + offset;
            var b1 = transform.TransformPoint(b * radius) + offset;
            var c1 = transform.TransformPoint(c * radius) + offset;
            Gizmos.DrawLine(a1, b1);
            Gizmos.DrawLine(a1, c1);
            Gizmos.DrawLine(b1, c1);
        });
    }

    void OnDrawGizmos(){
        drawSphereGrid();
        drawTetraGrid();
    }

    void OnEnable(){
        rebuildMeshes();
    }

    void OnDisable(){
        killObjects();
    }

    void Update(){
        if ((lastRadius == radius) && (lastGridSize == sphereGridSize)
            && (lastSubdivisions == numTetraSubdivisions))
            return;
        lastRadius = radius;
        lastGridSize = sphereGridSize;
        lastSubdivisions = numTetraSubdivisions;
        rebuildMeshes();
    }
}

Distortion of quads near corners can be minimized, to extent, but in turn it can make determining sector coordinates more difficult.

As far as I can tell, a good idea in general would be to determine quad based sector, put its center to origin, and align one of the sides with, say, global X coordinate.

1 Like

I’m not sure if this technique is directly applicable, but its certainly an interesting way to approach spherical worlds.

1 Like

I’ll have to think about this.
The problem I see is, the six connected grids. Say you are in the front terrain for face, in the small quad in the top right hand corner.

What is to your right? You could grab that quad, it’s the top left hand quad of the face to your right. Fine.
What about the quad above you? Is it the bottom right hand quad of the top face of the sphere? If so, then…what is above and to the right ? There isn’t a quad there. The faces of the sphere do not arrange themselves into a nice layout, there are gaps and missing quads around the player when you are in a corner.

My struggle (often) is I have a hard time visualizing abstract things, maybe there is some fundamental thing I’m missing. I do appreciate any insight you might have, though.

I’ll have to watch this when I get home from work, thanks BoredMormon!