LDraw + Unity. Как я Lego генерировал

Всех с наступающим! Меня зовут Гриша, и я основатель CGDevs. Уже не за горами праздники, кто-то уже нарядил ёлку, поел мандаринов и во всю заряжается новогодним настроением. Но сегодня речь пойдёт не об этом. Сегодня мы поговорим про замечательный формат под названием LDraw и про плагин для Unity, который я реализовал и выложил в OpenSource. Ссылка на проект и исходники к статье, как всегда, прилагаются. Если вы так же, как и я любите лего — добро пожаловать под кат.

okbqssjungymuljqjo7przp66ig.png
Формат LDraw
Начнём с того, что такое LDraw? LDraw — это открытый стандарт для LEGO CAD программ, позволяющий пользователям создавать модели и сцены LEGO. В целом существую разные программы и плагины, с помощью которых можно визуализировать LDraw (к примеру, есть плагин для Blender).
Сам формат хорошо задокументирован, и мы поговорим про его последнюю версию, а точнее про 1.0.2.

LDraw — это текстовый формат, файлы которого должны быть созданы с кодировкой UTF-8. Файлы, поддерживаемые форматом, должны иметь расширение ldr, dat или mdp. Каждая строка файла — это отдельная команда, отвечающая за определённую функцию.
Важной деталью формата является правосторонняя система координат (Y направлен вверх) — подробнее обсудим позже в контексте юнити, а также то, что формат является рекурсивным (большая часть файлов содержит указание на другие файлы)

ruyvnky8u4cxrfzqp5perilpuwc.png

Команды LDraw

В целом с этой информацией можно ознакомиться в официальной документации, но рассмотрим немного в контексте Unity. Всего формат LDraw поддерживает 6 типов команд.

0. Комментарий или мета команда — это специальные команды, которых мы почти не будем касаться в плагине. Пример: 0 !META command additional parameters

1. Ссылка на файл. По сути, самая сложная в интеграции и интересная команда. Выглядит она как — 1 colour x y z a b c d e f g h i file, где параметры являются TRS матрицей (подробнее про TRS можно прочитать в этой статье). В контексте юнити в форме
/ a d g 0 \
| b e h 0 |
| c f i 0 |
\ x y z 1 /

2. Линия — не используется в случае Unity, нужно чтобы подчеркнуть грани определённым цветом в CAD системах.

3,4. Треугольник и квадрат. Команды достаточно простые, но есть один важный нюанс, так как формат LDraw не рассчитан на 3д моделирование, то обход треугольников и квадратов в нём не стандартизирован. Это важно, так как-то же юнити в зависимости от обхода треугольника определяет направление calculated нормали, а также какая сторона треугольника является задней, а какая передней (что так же важно для отрисовки и куллинга)

Пример команд:
Треугольник — 3 colour x1 y1 z1 x2 y2 z2 x3 y3 z3
Квадрат — 4 colour x1 y1 z1 x2 y2 z2 x3 y3 z3 x4 y4 z4

5. Опциональная линия — тоже не используется.

n9l4c7gswhtafqylmnb-a2n91fo.png

Цвета в LDraw

Как можно заметить в большинстве команд, отвечающих за отрисовку, цвет идёт сразу после типа команды. Цвета хорошо задокументированы в этих двух статьях www.ldraw.org/article/299.html и www.ldraw.org/article/547.html, но поговорим про особенности, с которыми я столкнулся при реализации. Тут стоит чуть подробнее поговорить про форматы и так называемый «Scope» формата. В формате присутствуют 3 типа файлов.

DAT — по сути это базовые элементы из которых уже собираются детали, либо какие-то базовые детали. Если не рендерить отдельные детали — указанный в них цвет не важен. Чаще всего там стоят стандартные цвета официального стандарта.

LDR — это самое интересное, с точки зрения цветов, и где Scope играет роль. Правило довольно простое, хотя на сайте описано сложным языков. Если вы из одного ldr ссылаетесь на другой — игнорируйте цвет указанный в корневом.

Для примера часть файла 30051–1 — X-wing Fighter — Mini.mpd (X-wing на картинке выше):

Пример

1 71 -10 0 50 0 0 1 0 1 0 -1 0 0 60470a.dat
1 71 10 0 50 0 0 -1 0 1 0 1 0 0 60470a.dat
0 STEP
1 19 0 8 50 0 0 -1 0 1 0 1 0 0 4032b.dat
0 STEP
0 ROTSTEP 35 55 0 ABS
1 19 0 -16 0 0 0 -1 0 1 0 1 0 0 3623.dat
1 72 0 -16 50 0 0 -1 0 1 0 1 0 0 3022.dat
0 STEP
1 72 0 -8 -70 1 0 0 0 1 0 0 0 1 30051 - Nose.ldr

Во всех dat файлах мы учитываем указанный цвет, а в команде 1 72 0 -8 -70 1 0 0 0 1 0 0 0 1 30051 — Nose.ldr — игнорируем 72, и используем значения из файла 30051 — Nose.ldr.

MDP — это файл модели, чаще всего содержит в себе описание нескольких ldr файлов. С точки зрения цвета так же не особо важен. Единственное, что мы учитываем при парсинге — это мета-команду FILE.

drhb78vtu2nn63gqmvtvo9ecgdc.png

Модели в LDraw

Самое прекрасное в формате LDraw, что у него достаточно много фанатов среди любителей лего. Многие интересные наборы можно найти на официальном сайте omr.ldraw.org, но, помимо этого, многие можно найти на отдельных форумах.
Про формат поговорили, теперь пора поговорить немного про плагин для Unity.

26s3o_slknlem7mpvgvpqhxacig.png

Плагин для Unity

Плагин предоставляет возможность генерировать 3д модели на основе файлов LDraw. Результаты вы можете увидеть в картинках из статьи. Важно: если у вас слабое устройство, лучше открывайте только сцены mini в папке Demo. Модели не оптимизированы и всегда генерируют backface.

А теперь поговорим немного про реализацию. На данный момент поддержана большая часть описанного выше.

Одной из, пожалуй, самых главных особенностей являются разные системы координат. Проблема в том, что в формате правосторонняя система координат, а в Unity — левосторонняя. Что это, по сути, означает, что все повороты и TRS матрица будут работать неверно. Отрицательный Y обыграть просто — отражаем все координаты относительно Vector3.up и получаем нужные (умножаем на -1). Но вот в случае с TRS матрицей всё сложнее. Так как формат рекурсивный, то просто отражать матрицу — нельзя, так как Matrix.Identity везде превратится в матрицу отражения и каждая вложенность будет отражать нашу модель по оси Y, что приведёт к неправильному отображению (если сохранять положительный scale). Пока я пришёл к не совсем верному решению в виде того, что разрешил отрицательный scale, что нужно будет переделать в будущих версиях.

Вторая особенность, это ориентация треугольников. Для квадов реализовано то, чтобы треугольники смотрели в одну сторону:

Код подготовки для квадратов
public override void PrepareMeshData(List triangles, List verts)
{
        var v = _Verts;
        var nA = Vector3.Cross(v[1] - v[0], v[2] - v[0]);
        var nB = Vector3.Cross(v[1] - v[0], v[2] - v[0]);

        var vertLen = verts.Count;
        triangles.AddRange(new[]
        {
                vertLen + 1,
                vertLen + 2,
                vertLen, 
                vertLen + 1,
                vertLen + 3,
                vertLen + 2
        });
                
        var indexes = Vector3.Dot(nA, nB) > 0 ? new int[] {0, 1, 3, 2} : new int[] {0, 1, 2, 3};
        for (int i = 0; i < indexes.Length; i++)
        {
                verts.Add(v[indexes[i]]);
        }
}


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

Кроме того, из-за того, что формат рекурсивный, иерархическая система Unity пришлась как никогда кстати.

С помощью рекурсии в двух методах, мы генерируем нужные нам меши и применяем TRS (реализацию можно прочитать в прошлой статье), и таким образом получаем в удобном формате все необходимые для нас смещения:

Методы для генерации модели на сцене

public class LDrawModel
{
public GameObject CreateMeshGameObject(Matrix4x4 trs, Material mat = null, Transform parent = null)
    {
        if (_Commands.Count == 0) return null;
        GameObject go = new GameObject(_Name);
    
        var triangles = new List();
        var verts = new List();
    
        for (int i = 0; i < _Commands.Count; i++)
        {
            var sfCommand = _Commands[i] as LDrawSubFile;
            if (sfCommand == null)
            {
                _Commands[i].PrepareMeshData(triangles, verts);
            }
            else
            {
                sfCommand.GetModelGameObject(go.transform);
            }
        }
    
        if (mat != null)
        {
            var childMrs = go.transform.GetComponentsInChildren();
            foreach (var meshRenderer in childMrs)
            {
                meshRenderer.material = mat;
            }
        }
    
        if (verts.Count > 0)
        {
            var visualGO = new GameObject("mesh");
            visualGO.transform.SetParent(go.transform);
            var mf = visualGO.AddComponent();
    
            mf.sharedMesh = PrepareMesh(verts, triangles);
            var mr = visualGO.AddComponent();
            if (mat != null)
            {
                mr.sharedMaterial = mat;
              
            }
        }
        
        go.transform.ApplyLocalTRS(trs);
    
        go.transform.SetParent(parent);
        return go;
    }
}
public class LDrawSubFile : LDrawCommand
{
        public void GetModelGameObject(Transform parent)
        {
                _Model.CreateMeshGameObject(_Matrix, GetMaterial(), parent);
        }
}


И по итогу мы получаем такие красивые визуализации:
hw7wbjwt3ipjb5lxyuc5-1off5w.png
3pdcj3f6f1fnbp-gjinr_relvb0.png

Подробнее можно посмотреть в репозитории на Github.

В целом по развитию плагина очень много идей, хочется ввести такие функциональности, как:
1. Сглаживание некоторых форм
2. Генерация только front face
3. Конструктор и выгрузка моделей обратно в формат LDraw
4. По круче шейдер для пластика с subsurface scattering (и правильный набор материалов в целом)
5. Unwrap UV для лайтмапов
6. Оптимизация моделей (сейчас большинство состоят из 500к+, а к примеру модель эйфелевой башни 2.8 миллона полигонов)

Но и на данный момент, плагин позволяет использовать модельки из лего в Unity3d, что достаточно прикольно. (Все изображения для статьи сделаны с помощью плагина) Весь код проекта выложен под MIT лицензией, но вот лицензию на конкретные модели советую смотреть на ресурсах LDraw.

Спасибо за внимание, надеюсь вы узнали для себя что-то новое, и вас заинтересовал формат и плагин! Если будет время — буду продолжать его развивать и буду рад помощи в этом нелёгком деле.

© Habrahabr.ru