1

So this post is more or less a continuation of my last question here, so check that link if you're lost.

Basically, I need to load higher quality images applied to a material on a globe as the user zooms in the camera further. As it stands right now, I load all images at once depending on the zoom level, which is profoundly unoptimized at lower zoom levels, and impossible at higher ones since a texture2darray can only hold 2048 images.

What I need to know is, how could I only load the images that the camera is viewing, and not worry about ones off screen?

This may be a ludicrously specific problem, so I understand if I'm barking up the wrong tree here, but I figured I'd throw it out there and see if anyone has any insight.

Here's my current tiling class:

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

public class TilerHelper : MonoBehaviour
{
    MapTiler tiler = new MapTiler("G:/s2cloudless_4326_v1.0.0.sqlite"); //Function that loads from the sql table
    public Material mat; //The material
    Texture2DArray texArr; //The array that will be made into the final texture
    public Texture2D[] textures; //The images loaded from the sql table
    int size = 2048; //A texture2darray can only hold 2048 textures
    int count = 0; //Iterator for loading images
    int zoom = 1; //Determines which zoom level to load from the sql table
    int lastZoom = 1; //Determines whether the zoom level has moved
    CameraOrbit cam; //The main camera
    int initX = 3; //The x value of the image at zoom level 1
    int initY = 1;//The y value of the image at zoom level 1

    // Start is called before the first frame update
    void Start()
    {
        SetTiles(3, 1, 1, 4, 2); //This loads the first set of images at max distance when the program starts
        cam = Camera.main.gameObject.GetComponent<CameraOrbit>();
    }

    // Update is called once per frame
    void Update()
    {
        //These different zoom levels are based on the camera distance from the earth (large earth, so large numbers)
        if (cam.distance <= 90000) zoom = 12;
        else if (cam.distance <= 100000) zoom = 11;
        else if (cam.distance <= 110000) zoom = 10;
        else if (cam.distance <= 130000) zoom = 9;
        else if (cam.distance <= 150000) zoom = 8;
        else if (cam.distance <= 170000) zoom = 7;
        else if (cam.distance <= 190000) zoom = 6;
        else if (cam.distance <= 210000) zoom = 5;
        else if (cam.distance <= 230000) zoom = 4;
        else if (cam.distance <= 250000) zoom = 3;
        else if (cam.distance <= 270000) zoom = 2;
        else if (cam.distance >= 270000) zoom = 1;

        //If the camera has gone to a new zoom level...
        if(lastZoom != zoom)
        {
            if (zoom == 1) SetTiles(initX, initY, zoom, 4, 2); //Set it to 1 manually, since the formula won't work for it
            else
            {
                //This formula will load the correct images in the correct places regardless of zoom level
                int counter = 1;
                int resultX = initX;
                int resultY = initY;
                while(counter < zoom)
                {
                    resultX = resultX * 2 + 1;
                    resultY = resultY * 2 + 1;
                    counter++;
                }
                SetTiles(resultX, resultY, zoom, (int)Mathf.Pow(2, zoom + 1), (int)Mathf.Pow(2, zoom));
            }

            lastZoom = zoom;  //Update last zoom
        }
    }
    //The method that actually places the images
    void SetTiles(int x, int y, int z, int columns, int rows)
    {
        textures = new Texture2D[size]; //The array to hold all the textures
        //Load and place all the images according to passed x, y, and zoom level
        for (int i = 0; i <= x; i++)
        {
            for (int j = 0; j <= y; j++)
            {
                textures[count] = tiler.Read(i, j, z); //The z determines the zoom level, so I wouldn't want them all loaded at once
                count++;     
            }
        }
        count = 0; //Reset the counter

        //Instantiate the texture2darray
        texArr = new Texture2DArray(256, 256, textures.Length, TextureFormat.RGBA32, false, true);
        texArr.filterMode = FilterMode.Bilinear;
        texArr.wrapMode = TextureWrapMode.Clamp;
        //Set the texture2darray to contain all images loaded
        for (int i = 0; i < textures.Length; i++)
        {
            if (textures[i] == null) continue;
            texArr.SetPixels(textures[i].GetPixels(), i, 0);
        }
        //Apply the texture and set appropriate material values
        texArr.Apply();
        mat.SetTexture("_MainTexArray", texArr);
        Matrix4x4 matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0, -90, 0), Vector3.one);
        mat.SetMatrix("_Matrix", matrix);
        mat.SetInt("_COLUMNS", columns);
        mat.SetInt("_ROWS", rows);
    }
}

And here is my shader code

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Tiling"
{
    Properties
    {
        _MainTexArray("Tex", 2DArray) = "" {}
        _COLUMNS("Columns", Int) = 8
        _ROWS("Rows", Int) = 4
    }
        SubShader
        {
            Pass{
            Tags {"RenderType" = "Opaque"}
            Lighting Off
            ZWrite Off

                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #pragma require 2darray

                #include "UnityCG.cginc"

                struct appdata 
                {
                   float4 vertex : POSITION;
                   float3 normal : NORMAL;
                };
                struct v2f
                {
                    float4 pos : SV_POSITION;
                    float3 normal : TEXCOORD0;
                };

                float4x4 _Matrix;
                v2f vert(appdata v, float3 normal : TEXCOORD0)
                {
                    v2f o;
                    o.pos = UnityObjectToClipPos(v.vertex);
                    o.normal = mul(v.normal, _Matrix);
                    return o;
                }

                UNITY_DECLARE_TEX2DARRAY(_MainTexArray);

                int _ROWS;
                int _COLUMNS;

                #define PI 3.141592653589793

                inline float2 RadialCoords(float3 a_coords)
                {
                    float3 a_coords_n = normalize(a_coords);
                    float lon = atan2(a_coords_n.z, a_coords_n.x);
                    float lat = acos(a_coords_n.y);
                    float2 sphereCoords = float2(lon, lat) * (1.0 / PI);
                    return float2(sphereCoords.x * 0.5 + 0.5, 1 - sphereCoords.y);
                }

                float _UVClamp;

                float4 frag(v2f IN) : COLOR
                {
                    float2 equiUV = RadialCoords(IN.normal);

                    float2 texIndex;
                    float2 uvInTex = modf(equiUV * float2(_COLUMNS, _ROWS), texIndex);

                    int flatTexIndex = texIndex.x * _ROWS + texIndex.y;

                    return UNITY_SAMPLE_TEX2DARRAY(_MainTexArray,
                    float3(uvInTex, flatTexIndex));
                }
                ENDCG
            }
        }
}

Thank you.

Mike OD
  • 117
  • 1
  • 12
  • Did you get anywhere on this problem? It's a toughie – Ruzihm Oct 10 '19 at 20:32
  • 1
    Ahh, not really. Eventually I was able to find an Asset on the Unity store (World Political Map) that had its own custom tiling system, which more or less fulfilled my purpose. Though even looking at their code I'm still pretty lost as to how it works. – Mike OD Oct 11 '19 at 14:37
  • 1
    Ah, the solution was eldritch sorcery all along. – Ruzihm Oct 11 '19 at 14:38
  • 1
    Such is the life of a software engineer. Thanks for offering your thoughts on a solution though. – Mike OD Oct 11 '19 at 14:42
  • To future visitors [this](https://stackoverflow.com/q/70162448/1092820) post may be of some use – Ruzihm Dec 07 '21 at 22:45

1 Answers1

1

Interesting problem to try and solve without looping through a lot of tiles or pixels. Here's a partial answer that might help you find a solution.

There are two main problem here:

  1. Finding which "tiles" are visible to the camera.
  2. Loading those tiles into the shader and having it index them correctly.

Finding which "tiles" are visible to the camera

The best idea I can come up here is to try and find a way to project the camera's view of the sphere onto where the visible points are on an equirectangular projection.

You could sample for this with multiple ViewportPointToRay and collision checks, but it would be inaccurate and especially in the cases where the camera can see "around" the sphere and the rays don't collide with the sphere there.

There may be a formulaic approach to this but I'm not sure what to do.


Loading those tiles into the shader and having it index them correctly.

This is the much easier part if you can determine which equirectangular tiles need to be sent. If you can draw a rectangle that loops around horizontally around all the points that the camera can see, then you can just send the tiles that are partially or completely inside that "loading rectangle".

You'll also need to send the start & endpoints of the rectangle. So the shader can calculate how to change the entire-map texIndex coordinates to where that tile is stored in the "loading rectangle".

Basically these lines in the shader would probably be the same:

float2 equiUV = RadialCoords(IN.normal);

float2 texIndex;
float2 uvInTex = modf(equiUV * float2(_COLUMNS, _ROWS), texIndex);

...

return UNITY_SAMPLE_TEX2DARRAY(_MainTexArray,
        float3(uvInTex, flatTexIndex));

This line would have to change in some way to also take into account the loading rectangle parameters and how it horizontally wraps around the tile map:

int flatTexIndex = texIndex.x * _ROWS + texIndex.y;

Sorry about the vagueness, especially for the first part. Good luck.

Community
  • 1
  • 1
Ruzihm
  • 19,749
  • 5
  • 36
  • 48