image

Одно из преимуществ 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): привязывает материал к объекту, создавая для него текстуру и цвет.

Анатомия 3D-объекта начинается с его меша. Создание этого меша начинается с его вершин. Невидимые линии, соединяющие эти вершины, образуют треугольники, которые определяют базовую форму объекта.


Затем нормали и UV-данные задают затенение, цвет и текстуру. Данные меша хранятся в mesh filter, а mesh renderer использует эти данные для отрисовки объекта в сцене.

То есть псевдокод создания 3D-модели выглядит так:

  • Создаём новый меш под названием «myMesh».
  • Добавляем данные в свойства вершин и треугольников myMesh.
  • Создаём новый mesh filter под названием «myMeshFilter».
  • Присваиваем свойству меша myMeshFilter значение myMesh.

Разобравшись с основами, скачайте проект, распакуйте файлы и запустите заготовку проекта в Unity. Посмотрите на структуру папок в окне Project:


Описание папок:

  • Prefabs: содержит префаб Sphere, который будет использоваться для сохранения 3D-меша в процессе выполнения приложения.
  • Scenes: содержит три сцены, которые мы используем в этом туториале.
  • Editor: скрипты внутри этой папки дают нам в редакторе сверхвозможности, которые мы используем в разработке.
  • Scripts: здесь находятся скрипты времени выполнения, которые прикрепляются к GameObject и выполняются при нажатии на Play.
  • Materials: в этой папке хранится материал для меша.

В следующем разделе мы создадим custom editor, чтобы визуализировать создание 3D-меша.

Изменение мешей с помощью 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<MeshFilter>(); 
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");

Объяснение кода:

  1. Получает исходный меш oMesh из компонента MeshFilter.
  2. Копирует в новый экземпляр меша cMesh.
  3. Присваивает скопированный меш снова mesh filter.
  4. Обновляет локальные переменные.

Сохраните файл и вернитесь в Unity. В консоли отладки должно отображаться сообщение «Init & Cloned». Выберите GameObject Cube в Hierarchy и проверьте его свойства в Inspector. В Mesh Filter должен отображаться ассет меша под названием clone. Отлично! Это означает, что мы успешно клонировали меш.


В папке Editor перейдите к MeshInspector.cs. В OnInspectorGUI(), после второй строки кода добавьте следующее:

if (GUILayout.Button("Reset")) //1
{
    mesh.Reset(); //2
}

Объяснение кода:

  1. Этот код отрисовывает в Inspector кнопку Reset.
  2. При нажатии он вызывает в MeshStudy.cs функцию 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;
}

Объяснение кода:

  1. Проверка существования исходного и клонированного меша.
  2. Сброс cMesh на исходный меш.
  3. Присвоение cMesh oMeshFilter.
  4. Обновление локальных переменных.

Сохраните файл и вернитесь в Unity. В Inspector нажмите на кнопку Test Edit, чтобы исказить меш куба. Далее нажмите кнопку Reset; куб должен вернуться к исходной форме.


Объяснение вершин и треугольников в Unity


Меш состоит из вершин, соединённых рёбрами в треугольники. Треугольники задают базовую форму объекта.

Класс Mesh:

  • Вершины хранятся как массив значений Vector3.
  • Треугольники хранятся как массив integer, соответствующих индексам массива вершин.

То есть в простом меше Quad, состоящем из четырёх вершин и двух треугольников, данные меша будут выглядеть так:


Отображение вершин


Здесь мы хотим отобразить вершины куба в виде голубых точек.

В 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
{
    ShowPoint(i);
}

Объяснение кода:

  1. handleTransform получает из mesh значения Transform.
  2. handleRotation получает режим Rotation текущего шарнира.
  3. Обходим вершины меша и отрисовываем точки с помощью ShowPoint().

В функции ShowPoint(), сразу после комментария //draw dot добавим следующее:

Vector3 point = handleTransform.TransformPoint(mesh.vertices[index]);

Объяснение кода: эта строка преобразует локальную позицию вершины в координату в мировом пространстве.

В той же функции, в блоке if сразу после только что добавленной строки кода добавим следующее:

Handles.color = Color.blue;
point = Handles.FreeMoveHandle(point, handleRotation, mesh.handleSize, Vector3.zero, Handles.DotHandleCap);

Объяснение кода:

  1. Задаёт цвет, размер и позицию точки с помощью вспомогательного класса Handles.
  2. Handles.FreeMoveHandle() создаёт манипулятор неограниченного движения, упрощающий операцию перетаскивания, которая пригодится нам в следующем разделе.

Сохраните файл и вернитесь в Unity. Проверьте свойство куба в Inspector и убедитесь, что опция Move Vertex Point включена. Теперь вы должны увидеть, что меш на экране помечен несколькими голубыми точками. Вот и они — вершины меша куба! Попробуйте проделать это с другими 3D-объектами и понаблюдайте за результатами.


Перемещение отдельной вершины


Начнём с самого простого шага манипуляций с мешем — перемещения отдельной вершины.

Перейдите в MeshInspector.cs. Внутри функции ShowPoint(), сразу после комментария //drag и прямо перед закрывающими скобками блока if добавьте следующее:

if (GUI.changed) //1
{
    mesh.DoAction(index, handleTransform.InverseTransformPoint(point)); //2
}

Объяснение кода:

  1. GUI.changed отслеживает все изменения, происходящие с точками, и хорошо работает вместе с Handles.FreeMoveHandle() для распознавания операции перетаскивания.
  2. Для перетаскиваемой вершины функция mesh.DoAction() получает в качестве параметров её индекс и значения Transform. Так как значения Transform вершины находятся в мировом пространстве, мы преобразуем их в локальное пространство с помощью InverseTransformPoint().

Сохраните файл скрипта и перейдите в MeshStudy.cs. В DoAction(), после открывающих скобок добавим следующее:

PullOneVertex(index, localPos);

Затем добавим в функцию PullOneVertex() следующее:

vertices[index] = newPos; //1
cMesh.vertices = vertices; //2
cMesh.RecalculateNormals(); //3

Объяснение кода:

  1. Мы обновляем целевую вершину значением newPos.
  2. Присваиваем значения обновлённых вершин обратно cMesh.vertices.
  3. В RecalculateNormals() пересчитываем и перерисовываем меш, чтобы он соответствовал изменениям.

Сохраняем файл и возвращаемся в Unity. Попробуйте перетаскивать точки на кубе; увидели ли вы сломанный меш?


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

Нахождение всех похожих вершин


Визуально меш куба состоит из восьми вершин, шести сторон и 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<int> relatedVertices = FindRelatedVertices(targetVertexPos, false); //2
foreach (int i in relatedVertices) //3
{
    vertices[i] = newPos;
}
cMesh.vertices = vertices; //4
cMesh.RecalculateNormals();

Объяснение кода:

  1. получаем позицию целевой вершины, которая будет использоваться в качестве аргумента метода FindRelatedVertices().
  2. Этот метод возвращает список индексов (соответствующих вершинам), имеющих ту же позицию, что и целевая вершина.
  3. Цикл обходит весь список и присваивает соответствующим вершинам значение newPos.
  4. Присваиваем обновлённый vertices обратно cMesh.vertices. Затем вызываем RecalculateNormals() для перерисовки меша с новыми значениями.

Сохраните файл и вернитесь в Unity. Перетащите любую из вершин; теперь меш должен сохранять форму и не разрушаться.


Теперь, когда мы выполнили первый шаг в манипуляции мешами, сохраним сцену и перейдём к следующему разделу.

Манипулирование мешами


В этом разделе вы узнаете о манипулировании мешами в реальном времени. Существует множество способов, но в этом туториале мы рассмотрим наиболее простой вид манипуляций мешами, а именно перемещение заранее созданных вершин меша.

Сбор выбранных индексов


Начнём с выбора вершин, которые будем перемещать в реальном времени.

Откройте сцену 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
}

Объяснение кода:

  1. Задаёт и отображает вершины меша как тип Handles.Button.
  2. При нажатии он добавляет выбранный индекс в список pressed, mesh.selectedIndices.

В OnInspectorGUI(), перед закрывающей скобкой, добавим следующее:

if (GUILayout.Button("Clear Selected Vertices"))
{
    mesh.ClearAllData();
}

Объяснение кода: так мы добавляем в Inspector кнопку Reset для вызова mesh.ClearAllData().

Сохраните файл и откройте HeartMesh.cs из папки Scripts. В функцию ClearAllData() добавьте следующее:

selectedIndices = new List<int>();
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! Перед выходом включайте его.

Превращение сферы в сердце


Изменение вершин меша в реальном времени по сути состоит из трёх этапов:

  1. Копируем текущие вершины меша (до анимации) в mVertices.
  2. Выполняем вычисления и изменяем значения в mVertices.
  3. Обновляем текущие вершины меша с помощью mVertices при изменении на каждом этапе и позволяем Unity автоматически вычислять нормали.

Откройте HeartMesh.cs и перед функцией Start() следующие переменные:

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; 

Объяснение кода:

  1. Радиус области, на которую влияет целевая вершина.
  2. Сила перетаскивания.
  3. Длительность анимации.
  4. Текущий индекс списка selectedIndices.

В функции Init() перед блоком if добавим следующее:

currentIndex = 0; 

Объяснение кода: в начале игры currentIndex присваивается значение 0 — первый индекс списка selectedIndices.

В той же функции Init() перед закрывающей скобкой блока else добавим следующее:

StartDisplacement();

Объяснение кода: запускаем функцию StartDisplacement(), если isEditMode имеет значение false.

Внутрь функции StartDisplacement() добавим следующее:

targetVertex = oVertices[selectedIndices[currentIndex]]; //1
starttime = Time.time; //2
isAnimate = true;

Объяснение кода:

  1. Выделяем targetVertex, чтобы начать анимацию.
  2. Задаём время начала и изменяем значение isAnimate на true.

После функции StartDisplacement() создадим функцию FixedUpdate() со следующим кодом:

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<MeshFilter>().mesh;
            isAnimate = false;
            isMeshReady = true;
        }
    }
}

Объяснение кода:

  1. Функция FixedUpdate() выполняется в цикле с фиксированным FPS.
  2. Если isAnimate имеет значение false, то пропускаем следующий код.
  3. Изменяем runtime анимации.
  4. Если runtime находится в пределах duration, то получаем мировые координаты targetVertex и DisplaceVertices(), охватывая целевую вершину параметрами pullvalue и radiusofeffect.
  5. В противном случае время закончилось. Прибавляем к currentIndex единицу.
  6. Проверяем, находится ли currentIndex среди selectedIndices. Переходим к следующей вершине в списке с помощью StartDisplacement().
  7. В противном случае в конце списка изменяем данные oMesh на текущий меш и присваиваем isAnimate значение false, чтобы остановить анимацию.

В DisplaceVertices() добавим следующее:

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();

Объяснение кода:

  1. Квадрат радиуса.
  2. Обходим в цикле каждую вершину меша.
  3. Получаем sqrMagnitude между currentVertexPos и targetVertexPos.
  4. Если sqrMagnitude превышает sqrRadius, то переходим к следующей вершине.
  5. В противном случае продолжаем, определяя значение falloff, зависящее от расстояния distance текущей вершины от центральной точки области действия.
  6. Получаем новую позицию Vector3 и применяем её Transform к текущей вершине.
  7. При выходе из цикла присваиваем данным 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<MeshFilter>().mesh = pfMesh; //5
        PrefabUtility.ReplacePrefab(gameObj, pfRef, ReplacePrefabOptions.Default); //6
        Object.DestroyImmediate(gameObj); //7
    }
}

Объяснение кода:

  1. Задаёт path значение пути к объекту префаба CustomHeart.
  2. Создаёт два объекта из префаба CustomHeart, один для создания экземпляра как GameObject (pfObj), второй — как ссылки (pfRef).
  3. Создаёт из CustomHeart экземпляр ассета меша pfMesh. Если он не найден, создаёт новый меш, в противном случае очищает имеющиеся данные.
  4. Заполняет pfMesh новыми данными меша, а затем добавляет его как ассет в CustomHeart.
  5. Заполняет ассет меша в gameObj значением pfMesh.
  6. Заменяет CustomHeart на gameObj сопоставляя ранее существовавшие соединения.
  7. Мгновенно уничтожает gameObj.

Сохраните файл и перейдите в HeartMesh.cs. В общем методе SaveMesh(), после создания экземпляра nMesh добавьте следующее:

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

Объяснение кода:

  1. Простая кривая состоит из трёх точек. Задаём точки для первой кривой.
  2. Генерируем первую кривую с помощью Curve() и присваиваем её значения curve. Рисуемая кривая может отображаться в превью, если в качестве последнего параметра указать true.

Перейдите в CurveType2() и добавьте следующее:

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

Объяснение кода:

  1. Задаём точки для второй кривой.
  2. Генерируем вторую кривую с помощью Curve() и присваиваем её значения curve. Рисуемая кривая может отображаться в превью, если в качестве последнего параметра указать true.

В StartDisplacement(), перед закрывающей скобкой добавим следующее:

if (curveType == CurveType.Curve1)
{
    CurveType1();
} 
else if (curveType == CurveType.Curve2)
{
    CurveType2();
}

Объяснение кода: здесь мы проверяем выбранную пользователем опцию 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]);

Объяснение кода:

  1. Получаем позицию кривой на заданной distance и умножаем её значение y на force, чтобы получить increment.
  2. Создаём новый тип данных Vector3 для хранения новой позиции текущей вершины и соответствующим образом применяем её Transform.

Сохраните файл и вернитесь в Unity. Проверьте свойства в компоненте CustomHeart игрового объекта Child. Вы увидите раскрывающийся список, в котором можно выбрать Curve Type. В раскрывающемся списке Edit Type выберите Add Indices или Remove Indices, чтобы обновить список вершин и поэкспериментировать с разными настройками.


Чтобы увидеть подробные результаты для разных типов кривых, введите значения в соответствии со скриншотом:


Для списка Curve Type выберите значение Curve1, убедитесь, что для Edit Type выбрано None и нажмите Play. Вы должны увидеть, что меш расходится в паттерн. Покрутите модель, чтобы увидеть её в виде сбоку, и сравните результаты для обоих типов кривых. Здесь вы видите, как выбранный Curve Type влияет на смещение меша.



Вот и всё! Можете нажать на Clear Selected Vertices, чтобы сбросить Selected Indices и поэкспериментировать с собственными паттернами. Но не забывайте, что есть и другие факторы, которые будут влиять на конечный результат меша, а именно:

  • Величина радиуса.
  • Распределение вершин по области.
  • Позиция паттерна выбранных вершин.
  • Способ, выбранный для смещения.

Куда двигаться дальше?


Файлы готового проекта находятся в архиве проекта туториала.

Не останавливайтесь на этом! Попробуйте использовать более сложные техники, применяемые в туториале «Процедурная генерация лабиринтов в Unity».

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

Комментарии (3)


  1. UncleJey
    06.11.2018 14:38

    Вот жеж Америку открыл то.


    1. anfield343
      06.11.2018 18:00

      К чему столько негатива с грамматическими ошибками? Лично для меня, статья информативна, интересна и хорошо структурирована. Спасибо.


      1. UncleJey
        07.11.2018 08:11

        В какой из 20 букв негатив-то? Общедоступные и хорошо описанные фичи. Всяк, кто ходил по официальным документациям найдёт пользу сий статьи сильно сомнительной.