Беспорядок с MeshData
API MeshData в Unity быстрое и гибкое, но порядок вершинных атрибутов должен соответствовать объявленным вами дескрипторам. В этой статье показано, как из‑за несовпадения порядка полей в структуре треугольник может стать невидимым и получатся некорректные границы (bounds), а также как это исправить — выровняв порядок Position/Normal/TexCoord. Используйте исправленный пример как шаблон, чтобы избегать тонких ошибок рендеринга.
Jun 14, 2022
Привет, сообщество Unity! Хочу поговорить об улучшениях в Unity — Unity 2020.1 Mesh API Improvements. Чем они выделяются по сравнению с ранними версиями, к которым мы привыкли ещё со времён Unity 1.5? Ключевые преимущества — повышенная скорость, более эффективное использование памяти и лучшая интеграция с Job System. Для практического сравнения рекомендую посмотреть примеры в их GitHub в репозитории MeshApiExamples. Эти улучшения серьёзно прокачивают наши возможности разработки, напоминая, что такую мощь важно применять аккуратно.
Поделюсь примером, где всё пошло не по плану. Я пытался создать треугольник по точкам (0,0,0), (0,1,0) и (1,1,0). Однако в моём коде была критическая ошибка.
Вот пример кода
(внимание: он содержит ошибку):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static UnityEngine.Mesh;
using UnityEngine.Rendering;
public struct MeshStruct
{
//Textures and normals
public float3 Normal;
public float2 TexCoord0;
//Positions
public float3 Position;
}
public class MeshQuestion : MonoBehaviour
{
[SerializeField] private MeshFilter mFilter;
void Start()
{
MeshDataArray meshDataArr = AllocateWritableMeshData(1);
var meshData = meshDataArr[0];
//Allocate index and vector descriptors
meshData.SetIndexBufferParams(3, IndexFormat.UInt16);
meshData.SetVertexBufferParams(3,
new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, dimension: 3, stream: 0),
new VertexAttributeDescriptor(VertexAttribute.Normal, VertexAttributeFormat.Float32, dimension: 3, stream: 0),
new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, dimension: 2, stream: 0));
//Get index and vertex descriptors
var vertexData = meshData.GetVertexData<MeshStruct>(stream: 0);
var indexData = meshData.GetIndexData<ushort>();
//Write index buffer data (one CW triangles)
indexData[0] = 0; indexData[1] = 1; indexData[2] = 2;
//Write vertex buffer data (three points)
vertexData[0] = new MeshStruct() { Position = new float3(0, 0, 0), Normal = math.forward(), TexCoord0 = float2.zero };
vertexData[1] = new MeshStruct() { Position = new float3(0, 1, 0), Normal = math.forward(), TexCoord0 = float2.zero };
vertexData[2] = new MeshStruct() { Position = new float3(1, 1, 0), Normal = math.forward(), TexCoord0 = float2.zero };
//Init submesh descriptor
var submesh = new SubMeshDescriptor(indexStart: 0, indexCount: 3, topology: MeshTopology.Triangles);
meshData.subMeshCount = 1;
meshData.SetSubMesh(0, submesh);
mFilter.mesh = new Mesh();
ApplyAndDisposeWritableMeshData(meshDataArr, mFilter.mesh, MeshUpdateFlags.Default);
}
}
Результат выполнения этого кода — создание меша с 3 вершинами и 3 индексами, то есть одного треугольника. Но треугольник не виден. Дополнительно рассчитанные границы (bounds) у меша имеют нулевой размер, а центр смещён в точку (0,0,1).
Давайте разберёмся, почему так происходит.
Проблема в неверном порядке полей внутри моей структуры. Согласно документации Unity, атрибуты вершины в каждом потоке (stream) должны располагаться последовательно в определённом порядке:
- Position
- Normal
- Tangent
- Color
- TexCoord0
- TexCoord1
- TexCoord2
- TexCoord3
- TexCoord4
- TexCoord5
- TexCoord6
- TexCoord7
- BlendWeight
- BlendIndices.
Соблюдение этой последовательности гарантирует, что рендеринг‑система Unity корректно интерпретирует и использует данные. Теперь посмотрим на фактическую раскладку атрибутов вершины в приведённом фрагменте кода, чтобы найти расхождения с ожидаемым порядком.
А вот что у нас задано в SetVertexBufferParams:
meshData.SetVertexBufferParams(3,
new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, dimension: 3, stream: 0),
new VertexAttributeDescriptor(VertexAttribute.Normal, VertexAttributeFormat.Float32, dimension: 3, stream: 0),
new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, dimension: 2, stream: 0));
Размерности VertexAttributeDescriptor заданы как 3, 3, 2, и все — Float32. Это означает, что ожидается структура данных в порядке: позиции (3 float), нормали (3 float) и текстурные координаты (2 float).
При этом структура у нас идёт как float3 (для нормалей), float2 (для текстурных координат) и float3 (для позиций), что расходится с ожидаемой последовательностью PPPNNNTT (где PPP — координаты позиции xyz, NNN — нормали xyz, TT — текстурные координаты xy).
Из‑за этого несоответствия:
- Первый
float3в структуре, предназначенный для нормалей, интерпретируется как позиция. - Далее
float2и часть следующегоfloat3(конкретно, первый float), предназначенные для текстурных координат и начала позиции, интерпретируются как нормали. - Оставшиеся два float из последнего
float3, которые должны были быть концом позиции, принимаются за текстурные координаты.
Такая неверная интерпретация приводит к неправильным позициям вершин, в результате чего границы меша оказываются с центром в (0,0,1) и нулевым размером, а вершины отображаются некорректно.
К сожалению, простая перестановка аргументов в SetVertexBufferParams проблему не решает, и Unity выдаст предупреждение, если данные не соответствуют ожидаемой раскладке. Решение — выровнять порядок полей вашей структуры под ожидаемый Unity порядок вершинных атрибутов.
Чтобы всё работало корректно и избежать путаницы в данных:
- Всегда формируйте вершинные данные в порядке, ожидаемом Unity: Position, Normal, Tangent, Color, TexCoordX, BlendWeights и BlendIndices.
- Учитывайте, как Unity интерпретирует данные исходя из порядка и типа каждого атрибута в вашей структуре.
Приведя структуру вершинных данных к ожидаемому порядку, вы получите корректный результат рендеринга — например, правильно отображаемый треугольник. Помните: аккуратное обращение со структурой данных — ключ к эффективному использованию возможностей рендеринга Unity.
Правильная версия (MeshStructFixed)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static UnityEngine.Mesh;
using UnityEngine.Rendering;
public struct MeshStruct
{
//Positions
public float3 Position;
//Textures and normals
public float3 Normal;
public float2 TexCoord0;
}
public class MeshQuestion : MonoBehaviour
{
[SerializeField] private MeshFilter mFilter;
void Start()
{
MeshDataArray meshDataArr = AllocateWritableMeshData(1);
var meshData = meshDataArr[0];
//Allocate index and vector descriptors
meshData.SetIndexBufferParams(3, IndexFormat.UInt16);
meshData.SetVertexBufferParams(3,
new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, dimension: 3, stream: 0),
new VertexAttributeDescriptor(VertexAttribute.Normal, VertexAttributeFormat.Float32, dimension: 3, stream: 0),
new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, dimension: 2, stream: 0));
//Get index and vertex descriptors
var vertexData = meshData.GetVertexData<MeshStruct>(stream: 0);
var indexData = meshData.GetIndexData<ushort>();
//Write index buffer data (one CW triangles)
indexData[0] = 0; indexData[1] = 1; indexData[2] = 2;
//Write vertex buffer data (three points)
vertexData[0] = new MeshStruct() { Position = new float3(0, 0, 0), Normal = math.forward(), TexCoord0 = float2.zero };
vertexData[1] = new MeshStruct() { Position = new float3(0, 1, 0), Normal = math.forward(), TexCoord0 = float2.zero };
vertexData[2] = new MeshStruct() { Position = new float3(1, 1, 0), Normal = math.forward(), TexCoord0 = float2.zero };
//Init submesh descriptor
var submesh = new SubMeshDescriptor(indexStart: 0, indexCount: 3, topology: MeshTopology.Triangles);
meshData.subMeshCount = 1;
meshData.SetSubMesh(0, submesh);
mFilter.mesh = new Mesh();
ApplyAndDisposeWritableMeshData(meshDataArr, mFilter.mesh, MeshUpdateFlags.Default);
}
}
Будьте особенно внимательны при создании мешей и работе с данными в Unity! Это поможет избегать ошибок и сделать процесс разработки более гладким и продуктивным. Спасибо, что дочитали!


