Анатомия мешей
Меш состоит из треугольников, расположенных в 3D-пространстве так, чтобы создать впечатление замкнутого объекта. Треугольник определяется тремя угловыми точками или вершинами. В классе Mesh, все вершины хранятся в одном массиве и каждый треугольник задается с помощью трех целых чисел, которые соответствуют индексам в массиве вершин. Треугольники, также, собраны в единый массив целых чисел, которые берутся группами по три от начала этого массива, так чтобы элементы 0, 1 и 2 определили первый треугольник, 3, 4 и 5 определили второй, и так далее. Любая вершина может быть повторно использована во многих треугольниках по желанию, но есть причины, когда это может быть нежелательно, что объясняется ниже.
Освещение и нормали
Треугольников достаточно, чтобы определить основную форму объекта, но в большинстве случаев необходима дополнительная информация для отображения сетки. Чтобы объект был правильно затенен при освещении, для каждой вершины должен быть указан вектор нормали. Вектор нормали направлен наружу перпендикулярно поверхности сетки в положении вершины с которой он связан. При расчете затенения, каждая нормаль к вершине сравнивается с направлением падающего света, который также вектор. Если направления этих векторов параллельны, то поверхность получает свет в лоб в этой точке и полная яркость света будет использоваться для затенения. Свет, приходящий точно перпендикулярно вектору нормали не даст освещения поверхности в этой точке. Как правило, свет падает под углом к нормали и поэтому затенение будет где-то между полной яркостью и полной темнотой, в зависимости от угла.
Так как сетка состоит из треугольников, может показаться, что нормали на его углах будут точно перпендикулярны плоскости их треугольника. Однако на самом деле нормали интерполируются между треугольниками с получением среднего значения между соседними углами. Если все три нормали треугольника указывают в одном направлении, то треугольник будет равномерно освещен на всем протяжении. Эффектом того, что разные треугольники равномерно затенены будет то, что края будут очень четкими и отчетливыми. Это именно то, что требуется для модели куба или других объектов с острым краем, но интерполяция нормалей может быть использована для создания плавного затенения, чтобы представить изогнутую поверхность.
To get crisp edges, it is necessary to double up vertices at each edge since both of the two adjacent triangles will need their own separate normals. For curved surfaces, vertices will usually be shared along edges but a bit of intuition is often required to determine the best direction for the shared normals. A normal might simply be the average of the normals of the planes of the surrounding triangles. However, for an object like a sphere, the normals should just be pointing directly outward from the sphere’s centre.
Вызывая Mesh.RecalculateNormals, вы можете поручить Unity рассчитать нормали для вас, сделав некоторые предположения о “смысле” геометрии сетки; она предполагает, что вершины общие для нескольких треугольников обозначают гладкую поверхность в то время как удвоенные вершины указывают на четкие края. В большинстве случаев это не плохое приближение, однако RecalculateNormals будет спотыкаться в некоторых ситуациях текстурирования, когда вершины должны быть удвоены, хотя поверхность гладкая.
Текстурирование
В добавление к освещению, модель также обычно использует текстурирование для создания тонких деталей поверхности. Текстура подобна изображению, напечатанному на растягиваемой пленке или резине. Для каждого треугольника сетки, определяется треугольная площадь изображения текстуры и этот треугольник текстуры растягивается и сжимается, чтобы соответствовать треугольнику сетки. Чтобы сделать эту работу, в каждой вершине нужно сохранять координаты точки текстуры, которая ей соответствует. Эти координаты двумерны и масштабированы в диапазон 0..1 (0 означает нижний/левый угол изображения, а 1 верхний/правый). Чтобы избежать путаницы этих координат с декартовыми координатами в 3D-мире, они называются U и V, а не более привычно — X и Y, их обычно так и называют — UV координаты.
Подобно нормалям, текстурные координаты уникальны для каждой вершины и, таким образом, существуют ситуации, когда приходится дублировать вершины для получения различных UV значение вдоль ребра. Очевидный пример, это когда два соседних треугольника используют разделенные части текстуры (например, глаза на текстуре лица). Также, большинство полностью замкнутых объемов потребуют “шов”, где область текстуры заворачивается, и соединяется. Значения UV на одной стороне шва будут отличаться от тех, что на другой стороне.
Смотрите так же
- Страница использования класса Mesh.
- Mesh scripting class reference.
Меши (Meshes)
Meshes составляют большую часть создаваемых вами трёхмерных миров. Unity не имеет встроенных инструментов для моделирования геометрии, разве что за исключением некоторых плагинов Asset store. Но несмотря на это, Unity поддерживает работу со многими пакетами трёхмерной графики. Unity также поддерживает работу с мешами, которые состоят как из трёхсторонних, так и из четырёхсторонних полигонов. Неоднородные рациональные безье-сплайны (Nurbs), неоднородные рационально сглаживаемые сетки (Nurms) а также высокополигональные поверхности должны быть конвертированы в полигоны.
3D форматы
Импортирование мешей в Unity может быть выполнено с помощью двух основных типов файлов:
- Экспортированные 3D форматы файлов, такие как .FBX или .OBJ
- Собственные файлы 3D приложений, например такие как .Max и .Blend файлы из 3D Studio Max и Blender.
Любой из этих типов позволит вам добавлять свои меши в Unity, но есть соображения относительно того типа, который вы выберите:
Экспортированные 3D файлы
Unity может читать .FBX, .dae (Collada), .3DS, .dxf и .obj файлы, obj или Collada экспортеры могут быть найдены для многих приложений, точно также как FBX экспортеры могут быть найдены здесь
Преимущества:
- Экспортируйте только необходимые данные
- Проверяемые данные (перед импортированием в Unity, переимпортируйте в 3D пакет)
- Как правило файлы меньшего размера
- Поддерживает модульный подход — к примеру разными компонентами для интерактивности и типов коллизий
- Поддерживает другие 3D пакеты, чьи форматы не поддерживаются у нас напрямую
Недостатки:
- Может замедлять процесс прототипирования и итераций
- Легче потерять след между исходной (рабочий файл) и игровой версией данных (к примеру экспортированный FBX файл)
Собственные файлы 3D приложений
Unity также может импортировать, путём конвертации, файлы: Max, Maya, Blender, Cinema4D, Modo, Lightwave и Cheetah3D , например, .MAX, .MB, .MA и др.
Преимущества:
- Быстрый процесс итерации (для повторного импортирования в Unity сохраните исходный файл)
- Изначально просто
Недостатки:
- На машинах, задействованных в работе над Unity проектом, должны быть установлены лицензионные копии данного программного обеспечения
- Файлы, содержащие ненужные данные могут стать неоправданно большими
- Большие файлы могут замедлить процесс автосохранения
- Меньше проверяется, поэтому труднее устранить ошибки
Здесь находится перечень поддерживаемых пакетов трёхмерной графики, другие же чаще всего экспортируют вышеупомянутый тип файла.
Текстуры
При импорте меша, Unity попытается используя свой метод поиска, автоматически найти текстуры, используемые им. Сперва импортёр начнёт искать подпапку Textures, внутри папки с мешем или в папках уровнем выше. Если это не поможет, тогда по всей структуре проекта будет выполнен глобальный поиск всех имеющихся текстур. Конечно данный метод поиска значительно медленнее обычного и его главным недостатком является то, что в результатах поиска может появиться две и более текстур с одинаковым названием. В таком случае нет гарантий того, что нужная текстура будет найдена.
Создание и присвоение материала
Для каждого импортированного материала, Unity применит следующие правила:-
Если генерация материала отменена (иначе говоря если галочка Import Materials не выставлена), тогда будет назначен материал по-умолчанию. Если же генерация была включена, тогда произойдёт следующее:
- Unity будет использовать название для своего материала, основываясь на значении параметра Material Naming
- Unity попытается найти существующий материал с таким именем. Область поиска для поиска материала задаётся при помощи параметра Material Search
- Если Unity удастся найти существующий материал, тогда он будет использован в импортированной сцене, если же нет, тогда будет создан новый материал
Коллайдеры (Colliders)
В Unity используется два основных типа коллайдеров: Mesh Colliders и Primitive Colliders . Меш коллайдеры — это те компоненты, которые используют для себя данные импортированного меша и могут быть применены для создания столкновений с окружением. Когда вы в настройках импорта (Import Settings) активируете опцию Generate Colliders , меш коллайдер автоматически добавится в сцену вместе с импортированным мешем. Он будет рассматриваться как цельный до тех пор, пока он будет работать в контексте использования физической системой.
При перемещении своего объекта по сцене (к примеру машины), вы не можете использовать меш коллайдеры. Вместо этого, вам необходимо использовать примитивные коллайдеры. В этом случае вам необходимо отключить опцию Generate Colliders .
Анимации (Animations)
Анимации автоматически импортируются из сцены. Для более детального ознакомления с настройками импорта анимации посетите главу документации под названием подготовка компонентов и их импорт в системе анимации (Mecanim).
Карты нормалей и персонажи (Normal mapping and characters)
Если у вас есть модель персонажа с наложенной на него картой нормалей, взятой с высокополигональной модели, тогда вам необходимо будет импортировать в сцену версию модели для игры с Smoothing angle в 180 градусов. Таким образом можно предотвратить появление странно выглядящих швов в местах сочленения модели. Если же швы всё ещё останутся даже после применения данных настроек, тогда активируйте опцию Split tangents across UV seams .
Если вы конвертируете чёрно-белое изображение в карту нормалей, вам не следует об этом беспокоиться.
Формы смешивания (Blendshapes)
Unity поддерживает формы смешивания(Blendshapes) (которые ещё называют морфинговыми целями или вертексной анимацией). Unity может импортировать формы смешивания из таких форматов как .FBX (формы смешивания и контроль над анимацией) и .dae (только формы смешивания). Формы смешивания Unity также поддерживают вертексную анимацию на вершинах, нормалях и касательных. На меш одновременно может воздействовать как его его скин (Skin), так и формы смешивания. Все меши, которые были импортированы с формами смешивания будут использовать компонент SkinnedMeshRenderer (и не важно, был ли к нему применён до этого скин или нет). Анимация форм смешивания импортируются как часть обычной анимации — она попросту анимирует веса форм смешивания на компоненте SkinnedMeshRenderer.
Есть два способа импорта форм смешивания с нормалями:
- Если установить режим импорта Normals в положение Calculate , тогда для мешей будет использоваться та же последовательность просчёта нормалей что и для форм смешивания.
- Экспортируйте информацию о группах сглаживания в исходный файл. Таким образом Unity просчитает нормали из групп сглаживания как для меша, так и для форм смешивания.
Если вам нужны касательные на ваших формах смешивания, тогда выставите режим импорта Tangents в положение Calculate .
Советы (Hints)
- Слейте между собой как можно больше мешей в единое целое. И сделайте так, чтобы они использовали одни и те же материалы и текстуры. Это должно дать хороший прирост в производительности.
- Если в процессе работы в Unity вам придётся довольно часто сталкиваться с настройкой своих объектов, (применяя к ним физику, скрипты и другие полезности) то, чтобы в дальнейшем избавить себя от лишней головной боли, вам следует заранее позаботиться о правильном именовании своих объектов в том трёхмерном приложении, в котором они изначально были созданы. Потому как работать с большим количеством объектов, имеющих названия вроде pCube17 или Box42 мягко говоря не очень удобно.
- Работая в своём трёхмерном приложении старайтесь располагать свои модели в центре мировой системы координат. В дальнейшем это упростит их размещение в Unity.
- Если вершины меша изначально не имеют своих цветов, то при первом рендере Unity автоматически назначит всем вершинам белый цвет.
Редактор Unity отображает гораздо больше вершин и треугольников (по сравнению с тем, что отображается в моём трёхмерном приложении).
Так и есть. На что вы действительно должны обратить внимание, так это на то, какое количество вершин/треугольников на самом деле было послано для просчёта на графический процессор (GPU). В отличие от случаев, где материал требует, чтобы эти данные посылались на GPU дважды, такие вещи как твёрдые-нормали (hard-normals) и несмежные UV развёртки(non-contiguous UVs) намеренно отображают гораздо большее количество вершин/треугольников, чем есть на самом деле. В контексте UV и 3D пространства треугольники должны располагаться смежно, чтобы сформировать собой границу, поэтому при уменьшении количества треугольников на UV-швах, которые и должны были образовать собой границу и возникает эффект мнимого увеличения их количества.
Особенности работы с Mesh в Unity
Компьютерная графика, как известно, является основой игровой индустрии. В процессе создания графического контента мы неизбежно сталкиваемся с трудностями, связанными с разницей его представления в среде создания и в приложении. К этим трудностям прибавляются риски простой человеческой невнимательности. Учитывая масштабы разработки игр, такие проблемы возникают либо часто, либо в больших количествах.
Борьба с подобными трудностями навела нас на мысли об автоматизации и написании статей на эту тему. Большая часть материала коснется работы с Unity 3D, поскольку это основное средство разработки в Plarium Krasnodar. Здесь и далее в качестве графического контента будут рассматриваться 3D-модели и текстуры.
В этой статье мы поговорим об особенностях доступа к данным представления 3D-объектов в Unity. Материал будет полезен в первую очередь новичкам, а также тем разработчикам, которые нечасто взаимодействуют с внутренним представлением таких моделей.
О 3D-моделях в Unity — для самых маленьких
При стандартном подходе в Unity для рендеринга модели используются компоненты MeshFilter и MeshRenderer. MeshFilter ссылается на Mesh — ассет, который представляет модель. Для большинства шейдеров информация о геометрии является обязательной минимальной составляющей для отрисовки модели на экране. Данные же о текстурной развертке и костях анимации могут отсутствовать, если они не задействованы. Каким образом этот класс реализован внутри и как все там хранится, является тайной за энную сумму денег семью печатями.
Снаружи меш как объект предоставляет доступ к следующим наборам данных:
- vertices — набор позиций вершин геометрии в трехмерном пространстве с собственным началом координат;
- normals, tangents — наборы векторов-нормалей и касательных к вершинам, которые обычно используются для расчета освещения;
- uv, uv2, uv3, uv4, uv5, uv6, uv7, uv8 — наборы координат для текстурной развертки;
- colors, colors32 — наборы значений цвета вершин, хрестоматийным примером использования которых является смешивание текстур по маске;
- bindposes — наборы матриц для позиционирования вершин относительно костей;
- boneWeights — коэффициенты влияния костей на вершины;
- triangles — набор индексов вершин, обрабатываемых по 3 за раз; каждая такая тройка представляет полигон (в данном случае треугольник) модели.
Ядро движка (UnityEngine (native)) изолировано от скриптов разработчика, и обращение к его функционалу реализовано через библиотеку UnityEngine (C#). Фактически она является адаптером, поскольку большинство методов служат прослойкой для получения данных от ядра. При этом ядро и вся остальная часть, в том числе ваши скрипты, крутятся под разными процессами и скриптовая часть знает только список команд. Таким образом, прямой доступ к используемой ядром памяти из скрипта отсутствует.
О доступе к внутренним данным, или Насколько все может быть плохо
Для демонстрации того, насколько все может быть плохо, проанализируем объем очищаемой памяти Garbage Collector’ом на примере из документации. Для простоты профилирования завернем аналогичный код в Update метод.
public class MemoryTest : MonoBehaviour < public Mesh Mesh; private void Update() < for (int i = 0; i < Mesh.vertexCount; i++) < float x = Mesh.vertices[i].x; float y = Mesh.vertices[i].y; float z = Mesh.vertices[i].z; DoSomething(x, y, z); >> private void DoSomething(float x, float y, float z) < //nothing to do >>
Мы прогнали данный скрипт со стандартным примитивом — сферой (515 вершин). При помощи инструмента Profiler, во вкладке Memory можно посмотреть, сколько памяти было помечено для очистки сборщиком мусора в каждом из кадров. На нашей рабочей машине это значение составило ~9.2 Мб.
Это довольно много даже для нагруженного приложения, а мы здесь запустили сцену с одним объектом, на который навешен простейший скрипт.
Важно упомянуть об особенности компилятора .Net и об оптимизации кода. Пройдясь по цепочке вызовов, можно обнаружить, что обращение к Mesh.vertices влечет за собой вызов extern метода движка. Это не позволяет компилятору оптимизировать код внутри нашего Update() метода, несмотря на то, что DoSomething() пустой и переменные x, y, z по этой причине являются неиспользуемыми.
Теперь закешируем массив позиций на старте.
public class MemoryTest : MonoBehaviour < public Mesh Mesh; private Vector3[] _vertices; private void Start() < _vertices = Mesh.vertices; >private void Update() < for (int i = 0; i < _vertices.Length; i++) < float x = _vertices[i].x; float y = _vertices[i].y; float z = _vertices[i].z; DoSomething(x, y, z); >> private void DoSomething(float x, float y, float z) < //nothing to do >>
В среднем 6 Кб. Другое дело!
Такая особенность стала одной из причин, по которой нам пришлось реализовать собственную структуру для хранения и обработки данных меша.
Как это делаем мы
За время работы над крупными проектами возникла идея сделать инструмент для анализа и редактирования импортируемого графического контента. О самих методах анализа и трансформации поговорим в следующих статьях. Сейчас же рассмотрим структуру данных, которую мы решили написать для удобства реализации алгоритмов с учетом особенностей доступа к информации о меше.
Изначально эта структура выглядела так:
Здесь класс CustomMesh представляет, собственно, меш. Отдельно в виде Utility мы реализовали конвертацию из UntiyEngine.Mesh и обратно. Меш определяется своим массивом треугольников. Каждый треугольник содержит ровно три ребра, которые в свою очередь определены двумя вершинами. Мы решили добавить в вершины только ту информацию, которая нам необходима для анализа, а именно: позицию, нормаль, два канала текстурной развертки (uv0 для основной текстуры, uv2 для освещения) и цвет.
Спустя некоторое время возникла необходимость обращения вверх по иерархии. Например, чтобы узнать у треугольника, какому мешу он принадлежит. Помимо этого, обращение вниз из CustomMesh в Vertex выглядело вычурно, а необоснованный и значительный объем дублированных значений действовал на нервы. По этим причинам структуру пришлось переработать.
В CustomMeshPool реализованы методы для удобного управления и доступа ко всем обрабатываемым CustomMesh. За счет поля MeshId в каждой из сущностей имеется доступ к информации всего меша. Такая структура данных удовлетворяет требованиям к первоначальным задачам. Ее несложно расширить, добавив соответствующий набор данных в CustomMesh и необходимые методы — в Vertex.
Стоит отметить, что такой подход не оптимален по производительности. В то же время большинство реализованных нами алгоритмов ориентированы на анализ контента в редакторе Unity, из-за чего не приходится часто задумываться об объемах используемой памяти. По этой причине мы кешируем буквально все что можно. Реализованный алгоритм мы сначала тестируем, а затем рефакторим его методы и в некоторых случаях упрощаем структуры данных для оптимизации времени выполнения.
На этом пока все. В следующей статье мы расскажем о том, как редактировать уже внесенные в проект 3D-модели, и воспользуемся рассмотренной структурой данных.
Манипуляция мешами в реальном времени на Unity
Одно из преимуществ Unity в качестве платформы для разработки игр — её мощный 3D-движок. В этом туториале вы познакомитесь с миром 3D-объектов и манипуляций мешами.
В связи с ростом технологий виртуальной и дополненной реальности (VR/AR) большинство разработчиков сталкивается со сложными концепциями 3D-графики. Пусть этот туториал будет для них отправной точкой. Не волнуйтесь, здесь не будет сложной 3D-математики — только сердца, рисунки, стрелки и куча интересного!
Примечание: этот туториал предназначен для пользователей, знакомых с IDE Unity и имеющих определённый опыт программирования на C#. Если у вас нет таких знаний, то изучите сначала туториалы Introduction to Unity UI и Introduction to Unity Scripting.
Вам понадобится версия Unity не ниже 2017.3.1. Последнюю версию Unity можно скачать здесь. В этом туториале используются custom editor, подробнее о них можно узнать из туториала Extending the Unity Editor.
Приступаем к работе
Для начала познакомьтесь с основными терминами 3D-графики, которые позволят вам лучше понять туториал.
Базовые технические термины 3D-графики:
- Вершины (Vertices): каждая вершина — это точка в 3D-пространстве.
- Меш (Mesh): содержит все вершины, рёбра, треугольники, нормали и UV-данные модели.
- Mesh Filter: хранит данные меша модели.
- Mesh Renderer: рендерит данные меша в сцене.
- Нормали (Normals): вектор вершины или поверхности. Он направлен наружу, перпендикулярно поверхности меша.
- Линии/рёбра (Lines/Edges): невидимые линии, соединяющие вершины друг с другом.
- Треугольники (Triangles): формируются при соединении трёх вершин.
- UV-развёртка (UV Map): привязывает материал к объекту, создавая для него текстуру и цвет.
Затем нормали и UV-данные задают затенение, цвет и текстуру. Данные меша хранятся в mesh filter, а mesh renderer использует эти данные для отрисовки объекта в сцене.
То есть псевдокод создания 3D-модели выглядит так:
- Создаём новый меш под названием «myMesh».
- Добавляем данные в свойства вершин и треугольников myMesh.
- Создаём новый mesh filter под названием «myMeshFilter».
- Присваиваем свойству меша myMeshFilter значение myMesh.
- Prefabs: содержит префаб Sphere, который будет использоваться для сохранения 3D-меша в процессе выполнения приложения.
- Scenes: содержит три сцены, которые мы используем в этом туториале.
- Editor: скрипты внутри этой папки дают нам в редакторе сверхвозможности, которые мы используем в разработке.
- Scripts: здесь находятся скрипты времени выполнения, которые прикрепляются к GameObject и выполняются при нажатии на Play.
- Materials: в этой папке хранится материал для меша.
Изменение мешей с помощью Custom Editor
Откройте 01 Mesh Study Demo, находящееся в папке Scenes. В окне Scene вы увидите 3D-куб:
Прежде чем приступать к мешу, давайте взглянем на скрипт custom editor.
Изменение скрипта редактора
Выберите папку Editor в окне Project. Скрипты в этой папке добавляют функционал к редактору (Editor) во время разработки и недоступны в режиме Build.
Откройте MeshInspector.cs и просмотрите исходный код. Все скрипты Editor должны реализовывать класс Editor , его атрибут CustomEditor сообщает классу Editor , для какого типа объекта он является редактором. OnSceneGUI() — это метод события, позволяющий выполнять отрисовку в окне Scene; OnInspectorGUI() позволяет добавить в Inspector дополнительные элементы GUI.
В MeshInspector.cs перед началом класса MeshInspector добавим следующее:
[CustomEditor(typeof(MeshStudy))]
Объяснение кода: атрибут CustomEditor сообщает Unity, какой тип объекта может изменять класс custom editor.
В OnSceneGUI() перед EditMesh() добавим следующее:
mesh = target as MeshStudy; Debug.Log("Custom editor is running");
Объяснение кода: класс Editor имеет стандартную переменную target . Здесь target является преобразованием в MeshStudy . Теперь custom editor будет отрисовывать в окне Scene все GameObject и прикреплёнными к ним MeshStudy.cs. Добавление отладочных сообщений позволяет убедиться в консоли, что custom editor действительно выполняется.
Сохраним (Save) файл и вернёмся в Unity. Перейдите в папку Scripts и перетащите MeshStudy.cs на GameObject Cube в Hierarchy, чтобы прикрепить его.
Теперь в консоли должно выводиться сообщение «Custom editor is running», и это означает, что мы всё сделали верно! Можете удалить отладочное сообщение, чтобы оно не мешало нам в консоли.
Клонирование и сброс меша
При работе с 3D-мешем в режиме Edit при помощи custom editor будьте аккуратны, чтобы не перезаписать меш Unity по умолчанию. Если это произойдёт, то придётся перезапускать Unity.
Чтобы безопасно клонировать меш без перезаписи исходной формы, создадим копию меша из свойства MeshFilter.sharedmesh и присвоим его снова mesh filter.
Для этого дважды щёлкните на MeshStudy.cs в папке Scripts, чтобы открыть файл в редакторе кода. Этот скрипт наследует от класса MonoBehaviour , и его функция Start() не выполняется в режиме Edit.
В MeshStudy.cs перед началом класса MeshStudy добавим следующее:
[ExecuteInEditMode]
Объяснение кода: после добавления этого атрибута функция Start() будет выполняться и в режиме Play, и в режиме Edit. Теперь мы сначала можем создать экземпляр объекта меша и клонировать его.
В InitMesh() добавим следующий код:
oMeshFilter = GetComponent(); oMesh = oMeshFilter.sharedMesh; //1 cMesh = new Mesh(); //2 cMesh.name = "clone"; cMesh.vertices = oMesh.vertices; cMesh.triangles = oMesh.triangles; cMesh.normals = oMesh.normals; cMesh.uv = oMesh.uv; oMeshFilter.mesh = cMesh; //3 vertices = cMesh.vertices; //4 triangles = cMesh.triangles; isCloned = true; Debug.Log("Init & Cloned");
- Получает исходный меш oMesh из компонента MeshFilter .
- Копирует в новый экземпляр меша cMesh .
- Присваивает скопированный меш снова mesh filter.
- Обновляет локальные переменные.
В папке Editor перейдите к MeshInspector.cs. В OnInspectorGUI() , после второй строки кода добавьте следующее:
if (GUILayout.Button("Reset")) //1 < mesh.Reset(); //2 >
- Этот код отрисовывает в Inspector кнопку Reset.
- При нажатии он вызывает в MeshStudy.cs функцию Reset() .
if (cMesh != null && oMesh != null) //1 < cMesh.vertices = oMesh.vertices; //2 cMesh.triangles = oMesh.triangles; cMesh.normals = oMesh.normals; cMesh.uv = oMesh.uv; oMeshFilter.mesh = cMesh; //3 vertices = cMesh.vertices; //4 triangles = cMesh.triangles; >
- Проверка существования исходного и клонированного меша.
- Сброс cMesh на исходный меш.
- Присвоение cMesh oMeshFilter .
- Обновление локальных переменных.
Объяснение вершин и треугольников в Unity
Меш состоит из вершин, соединённых рёбрами в треугольники. Треугольники задают базовую форму объекта.
- Вершины хранятся как массив значений Vector3 .
- Треугольники хранятся как массив integer, соответствующих индексам массива вершин.
Отображение вершин
Здесь мы хотим отобразить вершины куба в виде голубых точек.
В MeshInspector.cs зайдём в функцию EditMesh() и добавим следующее:
handleTransform = mesh.transform; //1 handleRotation = Tools.pivotRotation == PivotRotation.Local ? handleTransform.rotation : Quaternion.identity; //2 for (int i = 0; i < mesh.vertices.Length; i++) //3
- handleTransform получает из mesh значения Transform.
- handleRotation получает режим Rotation текущего шарнира.
- Обходим вершины меша и отрисовываем точки с помощью ShowPoint() .
Vector3 point = handleTransform.TransformPoint(mesh.vertices[index]);
Объяснение кода: эта строка преобразует локальную позицию вершины в координату в мировом пространстве.
В той же функции, в блоке if сразу после только что добавленной строки кода добавим следующее:
Handles.color = Color.blue; point = Handles.FreeMoveHandle(point, handleRotation, mesh.handleSize, Vector3.zero, Handles.DotHandleCap);
- Задаёт цвет, размер и позицию точки с помощью вспомогательного класса Handles .
- Handles.FreeMoveHandle() создаёт манипулятор неограниченного движения, упрощающий операцию перетаскивания, которая пригодится нам в следующем разделе.
Перемещение отдельной вершины
Начнём с самого простого шага манипуляций с мешем — перемещения отдельной вершины.
Перейдите в MeshInspector.cs. Внутри функции ShowPoint() , сразу после комментария //drag и прямо перед закрывающими скобками блока if добавьте следующее:
if (GUI.changed) //1 < mesh.DoAction(index, handleTransform.InverseTransformPoint(point)); //2 >
- GUI.changed отслеживает все изменения, происходящие с точками, и хорошо работает вместе с Handles.FreeMoveHandle() для распознавания операции перетаскивания.
- Для перетаскиваемой вершины функция mesh.DoAction() получает в качестве параметров её индекс и значения Transform. Так как значения Transform вершины находятся в мировом пространстве, мы преобразуем их в локальное пространство с помощью InverseTransformPoint() .
PullOneVertex(index, localPos);
Затем добавим в функцию PullOneVertex() следующее:
vertices[index] = newPos; //1 cMesh.vertices = vertices; //2 cMesh.RecalculateNormals(); //3
- Мы обновляем целевую вершину значением newPos .
- Присваиваем значения обновлённых вершин обратно cMesh.vertices .
- В RecalculateNormals() пересчитываем и перерисовываем меш, чтобы он соответствовал изменениям.
Похоже, что некоторые из вершин имеют одинаковую позицию, поэтому когда мы перетаскиваем только одну, остальные вершины остаются за ней, и меш ломается. В следующем разделе мы устраним эту проблему.
Нахождение всех похожих вершин
Визуально меш куба состоит из восьми вершин, шести сторон и 12 треугольников. Давайте проверим, так ли это.
Откроем MeshStudy.cs, взглянем в место перед функцией Start() и найдём переменную vertices . Мы увидим следующее:
[HideInInspector] public Vector3[] vertices;
Объяснение кода: [HideInInspector] скрывает общую переменную от окна Inspector.
Закомментируем этот атрибут:
//[HideInInspector] public Vector3[] vertices;
Примечание: сокрытие значений вершин помогает [HideInInspector] в случае более сложных 3D-мешей. Так как размер массива вершин может достигать тысяч элементов, то это может приводить к торможению Unity при попытке просмотра значения массива в Inspector.
Сохраните файл и вернитесь в Unity. Перейдите в Inspector. Теперь под компонентом скрипта Mesh Study появилось свойство vertices. Нажмите на значок стрелки рядом с ним; так вы развернёте массив элементов Vector3 .
Можно увидеть, что размер массива равен 24, то есть существуют вершины, имеющие одинаковую позицию! Перед тем, как продолжать работу, не забудьте раскомментировать [HideInInspector] .
Почему вершин 24?
На этот счёт есть много теорий. Но простейший ответ таков: у куба шесть сторон, и каждая сторона составлена из четырёх вершин, образующих плоскость.
Поэтому расчёт таков: 6 x 4 = 24 вершины.
Можете поискать и другие ответы. Но пока достаточно просто знать, что у некоторых мешей будут вершины, имеющие одинаковую позицию.
В MeshStudy.cs заменим весь код внутри функции DoAction() на следующий:
PullSimilarVertices(index, localPos);
Перейдём в функцию PullSimilarVertices() и добавим следующее:
Vector3 targetVertexPos = vertices[index]; //1 List relatedVertices = FindRelatedVertices(targetVertexPos, false); //2 foreach (int i in relatedVertices) //3 < vertices[i] = newPos; >cMesh.vertices = vertices; //4 cMesh.RecalculateNormals();
- получаем позицию целевой вершины, которая будет использоваться в качестве аргумента метода FindRelatedVertices() .
- Этот метод возвращает список индексов (соответствующих вершинам), имеющих ту же позицию, что и целевая вершина.
- Цикл обходит весь список и присваивает соответствующим вершинам значение newPos .
- Присваиваем обновлённый vertices обратно cMesh.vertices . Затем вызываем RecalculateNormals() для перерисовки меша с новыми значениями.
Теперь, когда мы выполнили первый шаг в манипуляции мешами, сохраним сцену и перейдём к следующему разделу.
Манипулирование мешами
В этом разделе вы узнаете о манипулировании мешами в реальном времени. Существует множество способов, но в этом туториале мы рассмотрим наиболее простой вид манипуляций мешами, а именно перемещение заранее созданных вершин меша.
Сбор выбранных индексов
Начнём с выбора вершин, которые будем перемещать в реальном времени.
Откройте сцену 02 Create Heart Mesh из папки Scenes. В окне Scene вы увидите красную сферу. Выберите Sphere в Hierarchy и перейдите в Inspector. Вы увидите, что к объекту прикреплён компонент скрипта Heart Mesh.
Теперь нам нужно, чтобы скрипт Editor для этого объекта отображал вершины меша в окне Scene. Перейдите в папку Editor и дважды щёлкните на HeartMeshInspector.cs.
В функции ShowHandle() , внутри блока if добавьте следующее:
Handles.color = Color.blue; if (Handles.Button(point, handleRotation, mesh.pickSize, mesh.pickSize, Handles.DotHandleCap)) //1 < mesh.selectedIndices.Add(index); //2 >
- Задаёт и отображает вершины меша как тип Handles.Button .
- При нажатии он добавляет выбранный индекс в список pressed, mesh.selectedIndices .
if (GUILayout.Button("Clear Selected Vertices"))
Объяснение кода: так мы добавляем в Inspector кнопку Reset для вызова mesh.ClearAllData() .
Сохраните файл и откройте HeartMesh.cs из папки Scripts. В функцию ClearAllData() добавьте следующее:
selectedIndices = new List(); targetIndex = 0; targetVertex = Vector3.zero;
Объяснение кода: код очищает значения в selectedIndices и targetIndex . Также он обнуляет targetVertex .
Сохраните файл и вернитесь в Unity. Выберите Sphere и перейдите в Inspector к компоненту скрипта HeartMesh. Разверните Selected Indices, нажав на значок стрелки рядом с ним. Это позволит нам отслеживать каждую вершину, добавляемую в список.
Включите Is Edit Mode с помощью флажка рядом с ним. Благодаря этому в окне Scene будут отрисовываться вершины меша. При нажатии на синие точки в Selected Indices должны соответствующим образом меняться значения. Также протестируйте кнопку Clear Selected Vertices, чтобы убедиться, что она очищает все значения.
Примечание: в изменённом custom Inspector у нас есть опция для отображения/скрытия манипулятора transform с помощью Show Transform Handle. Так что не паникуйте, если не найдёте в других сценах манипулятор Transform! Перед выходом включайте его.
Превращение сферы в сердце
Изменение вершин меша в реальном времени по сути состоит из трёх этапов:
- Копируем текущие вершины меша (до анимации) в mVertices .
- Выполняем вычисления и изменяем значения в mVertices .
- Обновляем текущие вершины меша с помощью mVertices при изменении на каждом этапе и позволяем Unity автоматически вычислять нормали.
public float radiusofeffect = 0.3f; //1 public float pullvalue = 0.3f; //2 public float duration = 1.2f; //3 int currentIndex = 0; //4 bool isAnimate = false; float starttime = 0f; float runtime = 0f;
- Радиус области, на которую влияет целевая вершина.
- Сила перетаскивания.
- Длительность анимации.
- Текущий индекс списка selectedIndices .
currentIndex = 0;
Объяснение кода: в начале игры currentIndex присваивается значение 0 — первый индекс списка selectedIndices .
В той же функции Init() перед закрывающей скобкой блока else добавим следующее:
StartDisplacement();
Объяснение кода: запускаем функцию StartDisplacement() , если isEditMode имеет значение false.
Внутрь функции StartDisplacement() добавим следующее:
targetVertex = oVertices[selectedIndices[currentIndex]]; //1 starttime = Time.time; //2 isAnimate = true;
- Выделяем targetVertex , чтобы начать анимацию.
- Задаём время начала и изменяем значение isAnimate на true.
void FixedUpdate() //1 < if (!isAnimate) //2 < return; >runtime = Time.time - starttime; //3 if (runtime < duration) //4 < Vector3 targetVertexPos = oFilter.transform.InverseTransformPoint(targetVertex); DisplaceVertices(targetVertexPos, pullvalue, radiusofeffect); >else //5 < currentIndex++; if (currentIndex < selectedIndices.Count) //6 < StartDisplacement(); >else //7 < oMesh = GetComponent().mesh; isAnimate = false; isMeshReady = true; > > >
- Функция FixedUpdate() выполняется в цикле с фиксированным FPS.
- Если isAnimate имеет значение false, то пропускаем следующий код.
- Изменяем runtime анимации.
- Если runtime находится в пределах duration , то получаем мировые координаты targetVertex и DisplaceVertices() , охватывая целевую вершину параметрами pullvalue и radiusofeffect .
- В противном случае время закончилось. Прибавляем к currentIndex единицу.
- Проверяем, находится ли currentIndex среди selectedIndices . Переходим к следующей вершине в списке с помощью StartDisplacement() .
- В противном случае в конце списка изменяем данные oMesh на текущий меш и присваиваем isAnimate значение false, чтобы остановить анимацию.
Vector3 currentVertexPos = Vector3.zero; float sqrRadius = radius * radius; //1 for (int i = 0; i < mVertices.Length; i++) //2 < currentVertexPos = mVertices[i]; float sqrMagnitute = (currentVertexPos - targetVertexPos).sqrMagnitude; //3 if (sqrMagnitute >sqrRadius) < continue; //4 >float distance = Mathf.Sqrt(sqrMagnitute); //5 float falloff = GaussFalloff(distance, radius); Vector3 translate = (currentVertexPos * force) * falloff; //6 translate.z = 0f; Quaternion rotation = Quaternion.Euler(translate); Matrix4x4 m = Matrix4x4.TRS(translate, rotation, Vector3.one); mVertices[i] = m.MultiplyPoint3x4(currentVertexPos); > oMesh.vertices = mVertices; //7 oMesh.RecalculateNormals();
- Квадрат радиуса.
- Обходим в цикле каждую вершину меша.
- Получаем sqrMagnitude между currentVertexPos и targetVertexPos .
- Если sqrMagnitude превышает sqrRadius , то переходим к следующей вершине.
- В противном случае продолжаем, определяя значение falloff , зависящее от расстояния distance текущей вершины от центральной точки области действия.
- Получаем новую позицию Vector3 и применяем её Transform к текущей вершине.
- При выходе из цикла присваиваем данным oMesh изменённые значения mVertices , и заставляем Unity пересчитать нормали.
Источник техники Falloff
Исходная формула взята из файла пакета ассетов Procedural Examples, который можно бесплатно скачать из Unity Asset Store.
Сохраните файл и вернитесь в Unity. Выберите Sphere, перейдите к компоненту HeartMesh и попробуйте добавить несколько вершин в свойство Selected Indices. Отключите Is Edit mode и нажмите Play, чтобы посмотреть на результат своей работы.
Поэкспериментируйте со значениями Radiusofeffect, Pullvalue и Duration, чтобы получить разные результаты. Когда будете готовы, измените настройки в соответствии с показанным ниже скриншотом.
Нажмите на Play. Превратилась ли ваша сфера в сердце?
Поздравляю! В следующем разделе мы сохраним меш в префаб для дальнейшего использования.
Сохранение меша в реальном времени
Для сохранения процедурного меша в форме сердца в режиме Play необходимо подготовить префаб, дочерним элементом которого будет 3D-объект, а затем заменить его ассет меша новым с помощью скрипта.
В окне Project найдите CustomHeart в папке Prefabs. Нажмите на значок стрелки, чтобы развернуть его содержимое и выберите Child. Теперь вы видите в окне превью Inspector объект Sphere. Это префаб, который будет хранить данные нового меша.
Откройте HeartMeshInspector.cs. Внутри функции OnInspectorGUI() , перед закрывающей скобкой добавьте следующее:
if (!mesh.isEditMode && mesh.isMeshReady) < string path = "Assets/Prefabs/CustomHeart.prefab"; //1 if (GUILayout.Button("Save Mesh")) < mesh.isMeshReady = false; Object pfObj = AssetDatabase.LoadAssetAtPath(path, typeof(GameObject)); //2 Object pfRef = AssetDatabase.LoadAssetAtPath (path, typeof(GameObject)); GameObject gameObj = (GameObject)PrefabUtility.InstantiatePrefab(pfObj); Mesh pfMesh = (Mesh)AssetDatabase.LoadAssetAtPath(path, typeof(Mesh)); //3 if (!pfMesh) < pfMesh = new Mesh(); >else < pfMesh.Clear(); >pfMesh = mesh.SaveMesh(); //4 AssetDatabase.AddObjectToAsset(pfMesh, path); gameObj.GetComponentInChildren().mesh = pfMesh; //5 PrefabUtility.ReplacePrefab(gameObj, pfRef, ReplacePrefabOptions.Default); //6 Object.DestroyImmediate(gameObj); //7 > >
- Задаёт path значение пути к объекту префаба CustomHeart.
- Создаёт два объекта из префаба CustomHeart, один для создания экземпляра как GameObject ( pfObj ), второй — как ссылки ( pfRef ).
- Создаёт из CustomHeart экземпляр ассета меша pfMesh . Если он не найден, создаёт новый меш, в противном случае очищает имеющиеся данные.
- Заполняет pfMesh новыми данными меша, а затем добавляет его как ассет в CustomHeart.
- Заполняет ассет меша в gameObj значением pfMesh .
- Заменяет CustomHeart на gameObj сопоставляя ранее существовавшие соединения.
- Мгновенно уничтожает gameObj .
nMesh.name = "HeartMesh"; nMesh.vertices = oMesh.vertices; nMesh.triangles = oMesh.triangles; nMesh.normals = oMesh.normals;
Объяснение кода: возвращает ассет меша со значениями из меша в форме сердца.
Сохраните файл и вернитесь в Unity. Нажмите Play. После завершения анимации в Inspector появится кнопка Save Mesh. Нажмите на кнопку, чтобы сохранить новый меш, а затем остановите проигрыватель.
Перейдите в папку Prefabs и посмотрите на префаб CustomHeart. Вы должны увидеть, что теперь в объекте префаба CustomHeart есть совершенно новый меш в форме сердца.
Отличная работа!
Соединяем всё вместе
В предыдущей сцене функция DisplaceVertices() использовала формулу Falloff для определения силы перетаскивания, которая прикладывалась к каждой вершине в пределах заданного радиуса. Точка «затухания» (fall off), в которой сила перетаскивания начинает снижаться, зависит от использованного типа Falloff: Linear, Gaussian или Needle. Каждый тип создаёт в меше разные результаты.
В этом разделе мы рассмотрим другой способ манипулирования вершинами: с помощью заданной кривой. Взяв правило, что скорость равна расстоянию, поделённому на время (d=(v/t)), мы можем определить позицию вектора, ссылаясь на его расстояние, поделённое на время.
Использование способа с кривой
Сохраните текущую сцену и откройте 03 Customize Heart Mesh из папки Scenes. Вы увидите в Hierarchy экземпляр префаба CustomHeart. Нажмите на значок стрелки рядом с ним, чтобы развернуть его содержимое и выберите Child.
Просмотрите его свойства в Inspector. Вы увидите компонент Mesh Filter с ассетом Heart Mesh. Прикрепите к Child в качестве компонента скрипт Custom Heart. Теперь ассет должен смениться с HeartMesh на clone.
Далее откройте CustomHeart.cs из папки Scripts. Перед функцией Start() добавьте следующее:
public enum CurveType < Curve1, Curve2 >public CurveType curveType; Curve curve;
Объяснение кода: здесь создаётся общее перечисление (enum) под названием CurveType , после чего оно делается доступным из Inspector.
Перейдите в CurveType1() и добавьте следующее:
Vector3[] curvepoints = new Vector3[3]; //1 curvepoints[0] = new Vector3(0, 1, 0); curvepoints[1] = new Vector3(0.5f, 0.5f, 0); curvepoints[2] = new Vector3(1, 0, 0); curve = new Curve(curvepoints[0], curvepoints[1], curvepoints[2], false); //2
- Простая кривая состоит из трёх точек. Задаём точки для первой кривой.
- Генерируем первую кривую с помощью Curve() и присваиваем её значения curve . Рисуемая кривая может отображаться в превью, если в качестве последнего параметра указать true.
Vector3[] curvepoints = new Vector3[3]; //1 curvepoints[0] = new Vector3(0, 0, 0); curvepoints[1] = new Vector3(0.5f, 1, 0); curvepoints[2] = new Vector3(1, 0, 0); curve = new Curve(curvepoints[0], curvepoints[1], curvepoints[2], false); //2
- Задаём точки для второй кривой.
- Генерируем вторую кривую с помощью Curve() и присваиваем её значения curve . Рисуемая кривая может отображаться в превью, если в качестве последнего параметра указать true.
if (curveType == CurveType.Curve1) < CurveType1(); >else if (curveType == CurveType.Curve2)
Объяснение кода: здесь мы проверяем выбранную пользователем опцию curveType и соответствующим образом генерируем curve .
В DisplaceVertices() , внутри оператора цикла for перед закрывающими скобками добавим следующее:
float increment = curve.GetPoint(distance).y * force; //1 Vector3 translate = (vert * increment) * Time.deltaTime; //2 Quaternion rotation = Quaternion.Euler(translate); Matrix4x4 m = Matrix4x4.TRS(translate, rotation, Vector3.one); mVertices[i] = m.MultiplyPoint3x4(mVertices[i]);
- Получаем позицию кривой на заданной distance и умножаем её значение y на force , чтобы получить increment .
- Создаём новый тип данных Vector3 для хранения новой позиции текущей вершины и соответствующим образом применяем её Transform.
Чтобы увидеть подробные результаты для разных типов кривых, введите значения в соответствии со скриншотом:
Для списка Curve Type выберите значение Curve1, убедитесь, что для Edit Type выбрано None и нажмите Play. Вы должны увидеть, что меш расходится в паттерн. Покрутите модель, чтобы увидеть её в виде сбоку, и сравните результаты для обоих типов кривых. Здесь вы видите, как выбранный Curve Type влияет на смещение меша.
Вот и всё! Можете нажать на Clear Selected Vertices, чтобы сбросить Selected Indices и поэкспериментировать с собственными паттернами. Но не забывайте, что есть и другие факторы, которые будут влиять на конечный результат меша, а именно:
- Величина радиуса.
- Распределение вершин по области.
- Позиция паттерна выбранных вершин.
- Способ, выбранный для смещения.
Куда двигаться дальше?
Файлы готового проекта находятся в архиве проекта туториала.
Не останавливайтесь на этом! Попробуйте использовать более сложные техники, применяемые в туториале «Процедурная генерация лабиринтов в Unity».
Надеюсь, вам понравился этот туториал, а информация оказалась полезной. Особую благодарность я выражаю Джасперу Флику из Catlike Coding за его отличные туториалы, которые помогли мне собрать демо для моего проекта.