Беспорядок с 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) должны располагаться последовательно в определённом порядке:

  1. Position
  2. Normal
  3. Tangent
  4. Color
  5. TexCoord0
  6. TexCoord1
  7. TexCoord2
  8. TexCoord3
  9. TexCoord4
  10. TexCoord5
  11. TexCoord6
  12. TexCoord7
  13. BlendWeight
  14. BlendIndices.

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

public struct MeshStruct
{
    public float3 Normal;
    public float2 TexCoord0;
    public float3 Position;
}

А вот что у нас задано в 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! Это поможет избегать ошибок и сделать процесс разработки более гладким и продуктивным. Спасибо, что дочитали!