Подреждане на текстури на атлас правилно с персонализиран шейдър в Unity

Това е малко сложно, но се свежда до доста прост проблем, надявам се. И така, ето как става: използвам Unity, за да генерирам обект на карта на игра по време на изпълнение от bsp файл, който има цял куп върхове, лица, uvs, препратки към текстури и т.н. Създадените мрежи излизат точно както трябва да бъдат и всички текстури излизат добре. Има обаче един проблем, има толкова много мрежи, създадени с толкова много материали, което води до много извиквания за изтегляне, което прави програмата бавна. Така че потърсих начин да намаля повикванията за теглене и намерих решение. Комбинирайте всички мрежи в една голяма мрежа и създайте текстурен атлас, като комбинирате всички използвани текстури. Комбинирането на мрежите работи добре и комбинирането на текстурите също излиза страхотно. Тогава се сблъсках с проблема с uv картографирането. Така че намерих решение от бялата книга на 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, преди да стигна до шейдъра, защото имам достъп до размерите на текстурата по това време.

Ето кода, който използвах за изчисляване на 2 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 картографиране.

UV координатите за дадено лице от даден тип воксел тогава са ...

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

Във вашия случай вероятно имате различен начин за нанасяне на избраната текстура от вашия пакет, но по същество комбинацията от лице и тип в моя случай е това, което определя набора за uv картографиране, който искам.

След това това ви позволява да използвате вашата мрежа с всеки стандартен шейдър, вместо да разчитате на персонализирана логика на шейдъра.

person War    schedule 10.10.2015
comment
Благодаря ви за бързия и подробен отговор, но вече се опитах да създам атлас, където текстурите ще бъдат дублирани, така че да не се налага да редувам плочки и просто да задавам UV след това. За съжаление, картите, които зареждам, се състоят от твърде много текстури, така че компютърът ми няма достатъчно RAM, за да създаде такъв атлас. А целта на програмата, която правя е да може да се стартира от по-ниски устройства. Освен това текстурите, които използвам най-вече, са с еднакъв размер, но има някои, които са различни и затова не мога да избера решение, което ми позволява да изчисля UV директно от шейдъра. - person Oxters Wyzgowski; 10.10.2015
comment
Идеята зад това решение е, че многото текстури, които имате в изходното изображение на вашия атлас, могат да бъдат препратени, ако не можете да получите текстурата на атласа в RAM, тогава как да я прехвърлите на графичния процесор, за да изпълни вашия код? Ако стойността по подразбиране не е подходяща, тогава, както казах в отговора, не можете ли просто да замените setuptextures? - person War; 10.10.2015
comment
Атласът в този контекст е съдържанието на един файл с текстура, който може да съдържа много подтекстури... колекция от тях ли е вашият атлас? Ако е така, използвайте този код, за да работите с един файл с текстура, ще трябва да напишете производна реализация, която да обработва някои от вашите файлове с текстура - person War; 10.10.2015
comment
Причината е, че не доставям текстурите от папката с ресурси, така че не мога да направя атласа на ръка. Програмата получава текстурите, използвани от картата по време на изпълнение, и ги комбинира в атлас, използвайки функцията PackTexture() на Unity, която работи без да натоварва паметта. Но когато се опитах да направя персонализиран създател на атлас, който ръчно подрежда текстурите в атласа, RAM не можа да се справи, защото подреждаме всяка текстура една по една и почти всяка текстура трябва да бъде подредена от стотиците използвани текстури. - person Oxters Wyzgowski; 10.10.2015
comment
правилно, но няма да имате стотици текстури в един атлас и ако имате, те ще бъдат малки, защото unity има вградено ограничение за размера на текстурата от 4k * 4k, така че звучи сякаш трябва да създадете малки набори от текстури във всеки атлас, след което преминете различни комплекти към шейдъра за всяка мрежа, която планирате да изобразите - person War; 10.10.2015
comment
FYI: Не използвам вграденото atlasing на Unity ... imo това е глупост и не работи както бих очаквал - person War; 10.10.2015
comment
Е, има около 120 текстури в текущата карта, върху която тествам, и те се оказват добре в текстура 2048x2048. Те са малки, но все пак трябва да изглеждат добре. Атласът излиза на 21,8 mb и не мисля, че PackTexture() на Unity причинява проблема. Снимките, които публикувах, показват изкривяването, което се случва в комбинираната мрежа, но не и в отделните мрежи. Текстурите на мрежата се изкривяват по линиите на триъгълниците. Опитвам се да намаля броя на триъгълниците в мрежата, но все още не успях. - person Oxters Wyzgowski; 10.10.2015
comment
да, като гледам какво сте публикували, трябва да работи добре с горния код ... Имате ли впечатлението, че зарежда много текстури в ram? Това е само механизъм за предоставяне на 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