Преобразование Cubemap в Equirectangular Panorama

Я хочу преобразовать из карты куба [figure1] в равноугольную панораму [figure2].

Figure1 Рисунок1

Figure2 введите описание изображения здесь

Можно перейти от Spherical to Cubic (следующим образом: Преобразовать 2: 1 равноугольную панораму на карту куба), но потерял, как отменить его.

Рисунок 2 должен отображаться в сфере с использованием Unity.

Ответ 1

Предполагая, что входное изображение находится в следующем формате cubemap:

Cubemap image

Цель состоит в том, чтобы проецировать изображение в равноугольный формат следующим образом:

Equidedangular image

Алгоритм преобразования довольно прост. Чтобы рассчитать наилучшую оценку цвета на каждом пикселе в равноугольном изображении, получим карту cub с 6 граней:

  • Во-первых, вычислить полярные координаты, соответствующие каждому пикселю в сферическое изображение.
  • Во-вторых, использование полярных координат формирует вектор и определяет которая является лицом кубковой карты и какой пиксель которой обращен к вектору вранье; точно так же, как рабаст из центра куба попал в один из его сторон и конкретной точки на этой стороне.

Имейте в виду, что существует несколько методов для оценки цвета пикселя в равноугольном изображении с заданной нормированной координатой (u, v) на определенной грани cubemap. Самый простой метод, который является очень грубым приближением и будет использоваться в этом ответе для простоты, заключается в округлении координат к определенному пикселю и использовании этого пикселя. Другие более продвинутые методы могут вычислять среднее количество нескольких соседних пикселей.

Реализация алгоритма будет зависеть от контекста. Я сделал быструю реализацию в Unity3D С#, которая показывает, как реализовать алгоритм в реальном мире. Он работает на процессоре, есть много возможностей для улучшения, но его легко понять.

using UnityEngine;

public static class CubemapConverter
{
    public static byte[] ConvertToEquirectangular(Texture2D sourceTexture, int outputWidth, int outputHeight)
    {
        Texture2D equiTexture = new Texture2D(outputWidth, outputHeight, TextureFormat.ARGB32, false);
        float u, v; //Normalised texture coordinates, from 0 to 1, starting at lower left corner
        float phi, theta; //Polar coordinates
        int cubeFaceWidth, cubeFaceHeight;

        cubeFaceWidth = sourceTexture.width / 4; //4 horizontal faces
        cubeFaceHeight = sourceTexture.height / 3; //3 vertical faces


        for (int j = 0; j < equiTexture.height; j++)
        {
            //Rows start from the bottom
            v = 1 - ((float)j / equiTexture.height);
            theta = v * Mathf.PI;

            for (int i = 0; i < equiTexture.width; i++)
            {
                //Columns start from the left
                u = ((float)i / equiTexture.width);
                phi = u * 2 * Mathf.PI;

                float x, y, z; //Unit vector
                x = Mathf.Sin(phi) * Mathf.Sin(theta) * -1;
                y = Mathf.Cos(theta);
                z = Mathf.Cos(phi) * Mathf.Sin(theta) * -1;

                float xa, ya, za;
                float a;

                a = Mathf.Max(new float[3] { Mathf.Abs(x), Mathf.Abs(y), Mathf.Abs(z) });

                //Vector Parallel to the unit vector that lies on one of the cube faces
                xa = x / a;
                ya = y / a;
                za = z / a;

                Color color;
                int xPixel, yPixel;
                int xOffset, yOffset;

                if (xa == 1)
                {
                    //Right
                    xPixel = (int)((((za + 1f) / 2f) - 1f) * cubeFaceWidth);
                    xOffset = 2 * cubeFaceWidth; //Offset
                    yPixel = (int)((((ya + 1f) / 2f)) * cubeFaceHeight);
                    yOffset = cubeFaceHeight; //Offset
                }
                else if (xa == -1)
                {
                    //Left
                    xPixel = (int)((((za + 1f) / 2f)) * cubeFaceWidth);
                    xOffset = 0;
                    yPixel = (int)((((ya + 1f) / 2f)) * cubeFaceHeight);
                    yOffset = cubeFaceHeight;
                }
                else if (ya == 1)
                {
                    //Up
                    xPixel = (int)((((xa + 1f) / 2f)) * cubeFaceWidth);
                    xOffset = cubeFaceWidth;
                    yPixel = (int)((((za + 1f) / 2f) - 1f) * cubeFaceHeight);
                    yOffset = 2 * cubeFaceHeight;
                }
                else if (ya == -1)
                {
                    //Down
                    xPixel = (int)((((xa + 1f) / 2f)) * cubeFaceWidth);
                    xOffset = cubeFaceWidth;
                    yPixel = (int)((((za + 1f) / 2f)) * cubeFaceHeight);
                    yOffset = 0;
                }
                else if (za == 1)
                {
                    //Front
                    xPixel = (int)((((xa + 1f) / 2f)) * cubeFaceWidth);
                    xOffset = cubeFaceWidth;
                    yPixel = (int)((((ya + 1f) / 2f)) * cubeFaceHeight);
                    yOffset = cubeFaceHeight;
                }
                else if (za == -1)
                {
                    //Back
                    xPixel = (int)((((xa + 1f) / 2f) - 1f) * cubeFaceWidth);
                    xOffset = 3 * cubeFaceWidth;
                    yPixel = (int)((((ya + 1f) / 2f)) * cubeFaceHeight);
                    yOffset = cubeFaceHeight;
                }
                else
                {
                    Debug.LogWarning("Unknown face, something went wrong");
                    xPixel = 0;
                    yPixel = 0;
                    xOffset = 0;
                    yOffset = 0;
                }

                xPixel = Mathf.Abs(xPixel);
                yPixel = Mathf.Abs(yPixel);

                xPixel += xOffset;
                yPixel += yOffset;

                color = sourceTexture.GetPixel(xPixel, yPixel);
                equiTexture.SetPixel(i, j, color);
            }
        }

        equiTexture.Apply();
        var bytes = equiTexture.EncodeToPNG();
        Object.DestroyImmediate(equiTexture);

        return bytes;
    }
}

Чтобы использовать графический процессор, я создал шейдер, который выполняет одно и то же преобразование. Это намного быстрее, чем запуск пикселя преобразования по пикселям на CPU, но, к сожалению, Unity накладывает ограничения на ограничения на кубмапы, поэтому его использование ограничено в сценариях при использовании входного изображения с высоким разрешением.

Shader "Conversion/CubemapToEquirectangular" {
  Properties {
        _MainTex ("Cubemap (RGB)", CUBE) = "" {}
    }

    Subshader {
        Pass {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }      

            CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #pragma fragmentoption ARB_precision_hint_fastest
                //#pragma fragmentoption ARB_precision_hint_nicest
                #include "UnityCG.cginc"

                #define PI    3.141592653589793
                #define TWOPI 6.283185307179587

                struct v2f {
                    float4 pos : POSITION;
                    float2 uv : TEXCOORD0;
                };

                samplerCUBE _MainTex;

                v2f vert( appdata_img v )
                {
                    v2f o;
                    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                    o.uv = v.texcoord.xy * float2(TWOPI, PI);
                    return o;
                }

                fixed4 frag(v2f i) : COLOR 
                {
                    float theta = i.uv.y;
                    float phi = i.uv.x;
                    float3 unit = float3(0,0,0);

                    unit.x = sin(phi) * sin(theta) * -1;
                    unit.y = cos(theta) * -1;
                    unit.z = cos(phi) * sin(theta) * -1;

                    return texCUBE(_MainTex, unit);
                }
            ENDCG
        }
    }
    Fallback Off
}

Качество результирующих изображений может быть значительно улучшено за счет использования более сложного метода для оценки цвета пикселя во время преобразования или путем последующей обработки полученного изображения (или обоих, фактически). Например, изображение большего размера может быть сгенерировано для применения фильтра размытия, а затем уменьшит его до желаемого размера.

Я создал простой проект Unity с двумя мастерами редактора, которые показывают, как правильно использовать либо код С#, либо шейдер, показанный выше. Получи это здесь: https://github.com/Mapiarz/CubemapToEquirectangular

Не забудьте установить правильные параметры импорта в Unity для ваших входных изображений:

  • Точечная фильтрация
  • Формат Truecolor
  • Отключить mipmaps
  • Non Power 2: None (только для 2DTextures)
  • Включить чтение/запись (только для 2DTextures)

Ответ 2

cube2sphere автоматизирует весь процесс. Пример:

$ cube2sphere front.jpg back.jpg right.jpg left.jpg top.jpg bottom.jpg -r 2048 1024 -fTGA -ostitched