Правильная мозаика текстур атласа с помощью настраиваемого шейдера в Unity

Это немного сложно, но, надеюсь, сводится к довольно простой проблеме. Итак, вот как это происходит: я использую Unity для создания игрового объекта карты во время выполнения из файла bsp, который имеет целый набор вершин, граней, uvs, ссылок на текстуры и так далее. Созданные сетки получаются именно такими, какими они должны быть, и все текстуры получаются хорошо. Однако есть одна проблема: существует так много сеток, созданных из такого количества материалов, что приводит к многочисленным вызовам отрисовки, замедляющим работу программы. Итак, я искал способ уменьшить количество вызовов отрисовки и нашел решение. Объедините все сетки в одну большую сетку и создайте атлас текстур, объединив все используемые текстуры. Комбинирование сеток отлично работает, и комбинирование текстур тоже отлично получается. Затем я столкнулся с проблемой уф-картографии. Итак, я нашел решение из официального документа NVidia, чтобы создать собственный шейдер, который использует функцию tex2d для интерполяции текселя из текстуры с использованием позиций uv с их производными. Я думаю, это сработало бы, но на моих меше действительно странные треугольники, и я думаю, что они разрушают это решение. На изображениях ниже вы можете увидеть разницу, когда сетки объединены, а когда они разделены:

Комбинированные сетки с измененными UV и пользовательскими шейдерами

Отдельные сетки с исходными UV

Это код, который я использую в шейдере для установки цвета модели:

o.Albedo = tex2D (_MainTex, IN.uv2_BlendTex, ddx(IN.uv_MainTex), ddy(IN.uv_MainTex)).rgb;

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

Вот код, который я использовал для вычисления двух UV:

                Rect surfaceTextureRect = uvReMappers[textureIndex];
                Mesh surfaceMesh = allFaces[i].mesh;
                Vector2[] atlasTiledUVs = new Vector2[surfaceMesh.uv.Length];
                Vector2[] atlasClampedUVs = new Vector2[surfaceMesh.uv.Length];
                for (int j = 0; j < atlasClampedUVs.Length; j++)
                {
                    Vector2 clampedUV = new Vector2((surfaceMesh.uv[j].x - Mathf.Floor(surfaceMesh.uv[j].x)), (surfaceMesh.uv[j].y - Mathf.Floor(surfaceMesh.uv[j].y)));
                    float atlasClampedX = (clampedUV.x * surfaceTextureRect.width) + surfaceTextureRect.x;
                    float atlasClampedY = (clampedUV.y * surfaceTextureRect.height) + surfaceTextureRect.y;
                    atlasTiledUVs[j] = new Vector2((surfaceMesh.uv[j].x * surfaceTextureRect.width) + surfaceTextureRect.x, (surfaceMesh.uv[j].y * surfaceTextureRect.height) + surfaceTextureRect.y);
                    atlasClampedUVs[j] = new Vector2(atlasClampedX, atlasClampedY);
                    if (i < 10) { Debug.Log(i + " Original: " + surfaceMesh.uv[j] + " ClampedUV: " + clampedUV); }
                }
                surfaceMesh.uv = atlasTiledUVs;
                surfaceMesh.uv2 = atlasClampedUVs;

Массив uvReMappers - это массив Rect, созданный при использовании функции Texture2D PackTextures ().

Извините, что так долго, но вот мой вопрос: почему текстуры получаются искаженными. Это связано с тем, как сетки триангулированы, или с тем, как я написал собственный шейдер. И напоследок как я могу это исправить.

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


person Oxters Wyzgowski    schedule 10.10.2015    source источник


Ответы (3)


Я наконец решил проблему! Получается, что я не должен вычислять UV перед шейдером. Вместо этого я передал информацию, необходимую шейдеру, через UV, чтобы он мог напрямую вычислить новые позиции текселей.

Вот код перед шейдером:

Rect surfaceTextureRect = uvReMappers[textureIndex];
Mesh surfaceMesh = allFaces[i].mesh;
Vector2[] atlasTexturePosition = new Vector2[surfaceMesh.uv.Length];
Vector2[] atlasTextureSize = new Vector2[surfaceMesh.uv.Length];
for (int j = 0; j < atlasTexturePosition.Length; j++)
{
    atlasTexturePosition[j] = new Vector2(surfaceTextureRect.x, surfaceTextureRect.y);
    atlasTextureSize[j] = new Vector2(surfaceTextureRect.width, surfaceTextureRect.height);
}
surfaceMesh.uv2 = atlasTexturePosition;
surfaceMesh.uv3 = atlasTextureSize;

Вот код шейдера:

tex2D(_MainTex, float2((frac(IN.uv.x) * IN.uv3.x) + IN.uv2.x, (frac(IN.uv.y) * IN.uv3.y) + IN.uv2.y));
person Oxters Wyzgowski    schedule 11.10.2015

Я применил другой подход и создал атлас текстуры на процессоре, оттуда UV-отображение было таким же, как обычное UV-отображение, все, что мне нужно было сделать, это назначить текстуру информации о вершине из моего атласа ...

Мой сценарий - это настраиваемый воксельный движок, который может обрабатывать все, от Minecraft до рендеринга планет на основе вокселей, и я еще не нашел сценарий, с которым он еще не справился.

Вот мой код для атласа ...

using UnityEngine;
using Voxels.Objects;

namespace Engine.MeshGeneration.Texturing
{
    /// <summary>
    /// Packed texture set to be used for mapping texture info on 
    /// dynamically generated meshes.
    /// </summary>
    public class TextureAtlas
    {
        /// <summary>
        /// Texture definitions within the atlas.
        /// </summary>
        public TextureDef[] Textures { get; set; }

        public TextureAtlas()
        {
            SetupTextures();
        }

        protected virtual void SetupTextures()
        {
            // default for bas atlas is a material with a single texture in the atlas
            Textures = new TextureDef[]
            {
                new TextureDef 
                { 
                    VoxelType = 0, 
                    Faces =  new[] { Face.Top, Face.Bottom, Face.Left, Face.Right, Face.Front, Face.Back },
                    Bounds = new[] {
                        new Vector2(0,1), 
                        new Vector2(1, 1),
                        new Vector2(1,0),
                        new Vector2(0, 0)
                    }
                }
            };
        }


        public static TextureDef[] GenerateTextureSet(IntVector2 textureSizeInPixels, IntVector2 atlasSizeInPixels)
        {
            int x = atlasSizeInPixels.X / textureSizeInPixels.X;
            int z = atlasSizeInPixels.Z / textureSizeInPixels.Z;
            int i = 0;
            var result = new TextureDef[x * z];
            var uvSize = new Vector2(1f / ((float)x), 1f / ((float)z));

            for (int tx = 0; tx < x; tx++)
                for (int tz = 0; tz < z; tz++)
                {
                    // for perf, types are limited to 255 (1 byte)
                    if(i < 255)
                    {
                        result[i] = new TextureDef
                        {
                            VoxelType = (byte)i,
                            Faces = new[] { Face.Top, Face.Bottom, Face.Left, Face.Right, Face.Front, Face.Back },
                            Bounds = new[] {
                                new Vector2(tx * uvSize.x, (tz + 1f) * uvSize.y), 
                                new Vector2((tx + 1f) * uvSize.x, (tz + 1f) * uvSize.y),
                                new Vector2((tx + 1f) * uvSize.x, tz * uvSize.y),
                                new Vector2(tx * uvSize.x, tz * uvSize.y)
                            }
                        };

                        i++;
                    }
                    else
                        break;
                }

             return result;
        }
    }
}

А для определения текстуры в атласе ...

using UnityEngine;
using Voxels.Objects;

namespace Engine.MeshGeneration.Texturing
{
    /// <summary>
    /// Represents an area within the atlas texture 
    /// from which a single texture can be pulled.
    /// </summary>
    public class TextureDef
    {
        /// <summary>
        /// The voxel block type to use this texture for.
        /// </summary>
        public byte VoxelType { get; set; }

        /// <summary>
        /// Faces this texture should be applied to on voxels of the above type.
        /// </summary>
        public Face[] Faces { get; set; }

        /// <summary>
        /// Atlas start ref
        /// </summary>
        public Vector2[] Bounds { get; set; }
    }
}

Для пользовательских сценариев, когда мне нужен прямой контроль над UV-отображениями, я наследую атлас текстур, а затем переопределяю метод SetupTextures (), но почти во всех случаях я создаю атласы, в которых текстуры имеют одинаковый размер, поэтому простой вызов GenerateTextureSet сделает Уф-картографические расчеты, я считаю, вам нужны.

Тогда UV-координаты для данного лица данного типа вокселей равны ...

IEnumerable<Vector2> UVCoords(byte voxelType, Face face, TextureAtlas atlas)
        {
            return atlas.Textures
                .Where(a => a.VoxelType == voxelType && a.Faces.Contains(face))
                .First()
                .Bounds;
        }

В вашем случае у вас, вероятно, есть другой способ сопоставления с выбранной текстурой из вашего пакета, но, по сути, комбинация лица и типа в моем случае - это то, что определяет набор УФ-сопоставления, который я хочу.

Это позволяет вам использовать сетку с любым стандартным шейдером вместо того, чтобы полагаться на пользовательскую логику шейдера.

person War    schedule 10.10.2015
comment
Спасибо за быстрый и подробный ответ, но я уже пробовал создать атлас, в котором текстуры будут дублироваться, так что мне не придется мозаить, а просто потом просто установить UV. К сожалению, карты, которые я загружаю, содержат слишком много текстур, поэтому на моем компьютере недостаточно оперативной памяти для создания такого атласа. И цель программы, которую я делаю, состоит в том, чтобы ее можно было запускать с устройств более низкого уровня. Кроме того, текстуры, которые я использую, в основном имеют одинаковый размер, но есть и другие, поэтому я не могу найти решение, которое позволяет мне рассчитывать UV напрямую из шейдера. - person Oxters Wyzgowski; 10.10.2015
comment
Идея этого решения заключается в том, что на многие текстуры, которые у вас есть в исходном изображении атласа, можно ссылаться. Если вы не можете получить текстуру атласа в оперативной памяти, то как передать ее на графический процессор для запуска вашего кода? Если значение по умолчанию не подходит, то, как я сказал в ответе, не могли бы вы просто переопределить setuptexture? - person War; 10.10.2015
comment
Атлас в этом контексте - это содержимое одного файла текстуры, который может содержать множество субтекстур ... является ли ваш атлас их коллекцией? Если это так, используйте этот код для работы с одним файлом текстуры, вам нужно будет написать производную реализацию для обработки некоторых из ваших файлов текстуры. - person War; 10.10.2015
comment
Причина в том, что я не поставляю текстуры из папки ресурсов, поэтому я не могу сделать атлас вручную. Программа получает текстуры, используемые картой во время выполнения, и объединяет их в атлас с помощью функции Unity PackTexture (), которая работает без нагрузки на барабан. Но когда я попытался создать собственный создатель атласа, который вручную вставляет текстуры в атлас, барана не может справиться с этим, потому что мы разбиваем каждую текстуру мозаикой по одной, и почти каждая текстура должна быть выложена плиткой из сотен используемых текстур. - person Oxters Wyzgowski; 10.10.2015
comment
верно, но у вас не будет сотен текстур в одном атласе, и если вы это сделаете, они будут крошечными, потому что у единства есть встроенный предел размера текстуры 4k * 4k, поэтому похоже, что вам нужно создать небольшие наборы текстур в каждом атласе, а затем передать разные наборы шейдеров для каждой сетки, которую вы планируете рендерить - person War; 10.10.2015
comment
К вашему сведению: я не использую встроенный атласинг единства ... я думаю, это шары и не работает так, как я ожидал - person War; 10.10.2015
comment
На текущей карте, которую я тестирую, около 120 текстур, и они работают нормально с текстурой 2048x2048. Они маленькие, но все равно должны хорошо выглядеть. Размер атласа составляет 21,8 мб, и я не думаю, что Unity PackTexture () вызывает проблему. На фотографиях, которые я опубликовал, показано искажение, происходящее в объединенной сетке, но не в отдельных сетках. Текстуры на сетке искажаются по линиям треугольников. Я пытаюсь уменьшить количество треугольников в сетке, но мне это не удалось. - person Oxters Wyzgowski; 10.10.2015
comment
да, глядя на то, что вы опубликовали, должно нормально работать с приведенным выше кодом ... У вас сложилось впечатление, что он загружает много текстур в таран? Это всего лишь механизм для предоставления данных UV, ни один из этих кодов на самом деле не имеет отношения к самим текстурам, это вам нужно делать в другом месте :) - person War; 11.10.2015
comment
Сам атлас, я не думаю, потребует слишком много памяти, но мне нужно создать атлас из самих текстур. Я загружаю их во время выполнения, когда я читаю файл bsp карты, и каждый из них по отдельности имеет ширину или высоту не менее 512 пикселей или и то, и другое. Так что я думаю, что это займет большую часть барана. Спасибо за попытку помочь, я очень ценю это :) - person Oxters Wyzgowski; 11.10.2015

Вы должны превратить переданный TEXCOORD0 из процента пространства изображения в значение пикселя, использовать модуль, чтобы выяснить, какой пиксель он находится на мозаичной текстуре, а затем снова превратить его в процент изображения.

Вот код: вам нужно определить 2D переменные _MainTex и _PatternTex.

        struct v2f
        {
            float2 uv : TEXCOORD0;
            float4 vertex : SV_POSITION;
        };            


        float modFunction(float number, float divisor){
            //2018-05-24: copied from an answer by Nicol Bolas: https://stackoverflow.com/questions/35155598/unable-to-use-in-glsl
            return (number - (divisor * floor(number/divisor)));
        }


        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 curColor = tex2D(_MainTex, i.uv);                
            fixed4 pattern = tex2D(_PatternTex, 
                float2(
                    modFunction(i.uv.x*_MainTex_TexelSize.z,_PatternTex_TexelSize.z) *_PatternTex_TexelSize.x,
                    modFunction(i.uv.y*_MainTex_TexelSize.w,_PatternTex_TexelSize.w) *_PatternTex_TexelSize.y
                )
            );
            fixed4 col = curColor * pattern;                
            col.rgb *= col.a;
            return col;
        }
person shieldgenerator7    schedule 24.05.2018