Правильное разбиение текстур на атлас с помощью пользовательского шейдера в Unity
Это немного сложно, но все сводится к простой проблеме, я надеюсь. Итак, вот как это происходит: я использую Unity для создания игрового объекта карты во время выполнения из файла bsp, который имеет целую кучу вершин, граней, uvs, ссылок на текстуры и так далее. Созданные сетки получаются именно такими, какими они должны быть, а все текстуры получаются хорошими. Однако есть одна проблема: существует так много мешей, созданных с таким количеством материалов, что приводит к множеству вызовов отрисовки, замедляющих работу программы. Поэтому я искал способ уменьшить количество раздач и нашел решение. Объедините все сетки в одну большую сетку и создайте текстурный атлас, объединив все используемые текстуры. Комбинация мешей работает отлично, а комбинирование текстур также получается великолепно. Тогда я столкнулся с проблемой ультрафиолетового картирования. Таким образом, я нашел решение из белой бумаги NVidia, чтобы сделать собственный шейдер, который использует функцию tex2d для интерполяции текселя из текстуры, используя uv-позиции с их производными. Я думаю, что это сработало бы, но мои сетки имеют действительно странные треугольники, и я думаю, что они разрушают это решение. На изображениях ниже вы можете видеть разницу, когда сетки объединяются с тем, когда они разделены:
Комбинированные сетки с измененными UV и пользовательским шейдером
Отдельные сетки с оригинальными ультрафиолетом
Вот код, который я использую в шейдере, чтобы установить цвет модели:
o.Albedo = tex2D (_MainTex, IN.uv2_BlendTex, ddx(IN.uv_MainTex), ddy(IN.uv_MainTex)).rgb;
Как вы можете видеть, я добавил второй UV, который является не оригинальной версией UV. Я делаю это с помощью функции frac(), но в коде C#, а не в шейдере. Поскольку текстуры могут быть разных размеров, мне пришлось рассчитать УФ, прежде чем перейти к шейдеру, потому что у меня есть доступ к размерам текстур в то время.
Вот код, который я использовал для расчета 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().
Извините, что так долго, но вот мой вопрос: почему текстуры получаются искаженными. Это из-за того, как сетки триангулированы, или из-за того, как я написал собственный шейдер. И наконец, как я могу это исправить.
Спасибо за ваше время. Я прошу прощения за то, что написал так много, но я никогда не публиковал вопрос раньше. Я всегда нахожу ответы почти на все мои проблемы в Интернете, но я искал несколько дней, как решить эту проблему. Я чувствую, что это может быть слишком конкретным, чтобы найти ответ. Я надеюсь, что предоставил достаточно информации.
3 ответа
Я наконец-то решил проблему! Так что получается, что я не должен вычислять 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));
Я выбрал другой подход и создал атлас текстур на процессоре, оттуда UV-отображение было похоже на обычное UV-отображение, все, что мне нужно было сделать, это присвоить текстуру информации о вершинах из моего атласа...
Мой сценарий - это собственный воксельный движок, который может обрабатывать что угодно, от майнкрафта до рендеринга планет на основе вокселей, и я еще не нашел сценарий, который он не может обработать.
Вот мой код для атласа...
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;
}
В вашем случае у вас, вероятно, есть другой способ сопоставления с выбранной текстурой из вашего пакета, но, по сути, комбинация лица и типа в моем случае - это то, что определяет набор ультрафиолетового отображения, который я хочу.
Это тогда позволяет вам использовать ваш меш с любым стандартным шейдером вместо того, чтобы полагаться на собственную логику шейдера.
Вы должны превратить пройденное в 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://stackru.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;
}