Как вывернуть нормали в unity
Манипуляция мешами в реальном времени на 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-графики:
Затем нормали и UV-данные задают затенение, цвет и текстуру. Данные меша хранятся в mesh filter, а mesh renderer использует эти данные для отрисовки объекта в сцене.
То есть псевдокод создания 3D-модели выглядит так:
Изменение мешей с помощью Custom Editor
Откройте 01 Mesh Study Demo, находящееся в папке Scenes. В окне Scene вы увидите 3D-куб:
Прежде чем приступать к мешу, давайте взглянем на скрипт custom editor.
Изменение скрипта редактора
Выберите папку Editor в окне Project. Скрипты в этой папке добавляют функционал к редактору (Editor) во время разработки и недоступны в режиме Build.
В MeshInspector.cs перед началом класса MeshInspector добавим следующее:
Объяснение кода: атрибут CustomEditor сообщает Unity, какой тип объекта может изменять класс custom editor.
В OnSceneGUI() перед EditMesh() добавим следующее:
Сохраним (Save) файл и вернёмся в Unity. Перейдите в папку Scripts и перетащите MeshStudy.cs на GameObject Cube в Hierarchy, чтобы прикрепить его.
Теперь в консоли должно выводиться сообщение «Custom editor is running», и это означает, что мы всё сделали верно! Можете удалить отладочное сообщение, чтобы оно не мешало нам в консоли.
Клонирование и сброс меша
При работе с 3D-мешем в режиме Edit при помощи custom editor будьте аккуратны, чтобы не перезаписать меш Unity по умолчанию. Если это произойдёт, то придётся перезапускать Unity.
Чтобы безопасно клонировать меш без перезаписи исходной формы, создадим копию меша из свойства MeshFilter.sharedmesh и присвоим его снова mesh filter.
В MeshStudy.cs перед началом класса MeshStudy добавим следующее:
Объяснение кода: после добавления этого атрибута функция Start() будет выполняться и в режиме Play, и в режиме Edit. Теперь мы сначала можем создать экземпляр объекта меша и клонировать его.
В InitMesh() добавим следующий код:
Объяснение вершин и треугольников в Unity
Меш состоит из вершин, соединённых рёбрами в треугольники. Треугольники задают базовую форму объекта.
Отображение вершин
Здесь мы хотим отобразить вершины куба в виде голубых точек.
В MeshInspector.cs зайдём в функцию EditMesh() и добавим следующее:
Объяснение кода: эта строка преобразует локальную позицию вершины в координату в мировом пространстве.
В той же функции, в блоке if сразу после только что добавленной строки кода добавим следующее:
Перемещение отдельной вершины
Начнём с самого простого шага манипуляций с мешем — перемещения отдельной вершины.
Затем добавим в функцию PullOneVertex() следующее:
Похоже, что некоторые из вершин имеют одинаковую позицию, поэтому когда мы перетаскиваем только одну, остальные вершины остаются за ней, и меш ломается. В следующем разделе мы устраним эту проблему.
Нахождение всех похожих вершин
Визуально меш куба состоит из восьми вершин, шести сторон и 12 треугольников. Давайте проверим, так ли это.
Объяснение кода: [HideInInspector] скрывает общую переменную от окна Inspector.
Закомментируем этот атрибут:
Примечание: сокрытие значений вершин помогает [HideInInspector] в случае более сложных 3D-мешей. Так как размер массива вершин может достигать тысяч элементов, то это может приводить к торможению Unity при попытке просмотра значения массива в Inspector.
На этот счёт есть много теорий. Но простейший ответ таков: у куба шесть сторон, и каждая сторона составлена из четырёх вершин, образующих плоскость.
Поэтому расчёт таков: 6 x 4 = 24 вершины.
Можете поискать и другие ответы. Но пока достаточно просто знать, что у некоторых мешей будут вершины, имеющие одинаковую позицию.
В MeshStudy.cs заменим весь код внутри функции DoAction() на следующий:
Перейдём в функцию PullSimilarVertices() и добавим следующее:
Теперь, когда мы выполнили первый шаг в манипуляции мешами, сохраним сцену и перейдём к следующему разделу.
Манипулирование мешами
В этом разделе вы узнаете о манипулировании мешами в реальном времени. Существует множество способов, но в этом туториале мы рассмотрим наиболее простой вид манипуляций мешами, а именно перемещение заранее созданных вершин меша.
Сбор выбранных индексов
Начнём с выбора вершин, которые будем перемещать в реальном времени.
Откройте сцену 02 Create Heart Mesh из папки Scenes. В окне Scene вы увидите красную сферу. Выберите Sphere в Hierarchy и перейдите в Inspector. Вы увидите, что к объекту прикреплён компонент скрипта Heart Mesh.
Теперь нам нужно, чтобы скрипт Editor для этого объекта отображал вершины меша в окне Scene. Перейдите в папку Editor и дважды щёлкните на HeartMeshInspector.cs.
Сохраните файл и откройте HeartMesh.cs из папки Scripts. В функцию ClearAllData() добавьте следующее:
Сохраните файл и вернитесь в Unity. Выберите Sphere и перейдите в Inspector к компоненту скрипта HeartMesh. Разверните Selected Indices, нажав на значок стрелки рядом с ним. Это позволит нам отслеживать каждую вершину, добавляемую в список.
Включите Is Edit Mode с помощью флажка рядом с ним. Благодаря этому в окне Scene будут отрисовываться вершины меша. При нажатии на синие точки в Selected Indices должны соответствующим образом меняться значения. Также протестируйте кнопку Clear Selected Vertices, чтобы убедиться, что она очищает все значения.
Примечание: в изменённом custom Inspector у нас есть опция для отображения/скрытия манипулятора transform с помощью Show Transform Handle. Так что не паникуйте, если не найдёте в других сценах манипулятор Transform! Перед выходом включайте его.
Превращение сферы в сердце
Изменение вершин меша в реальном времени по сути состоит из трёх этапов:
В той же функции Init() перед закрывающей скобкой блока else добавим следующее:
Внутрь функции StartDisplacement() добавим следующее:
Источник техники 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. Это префаб, который будет хранить данные нового меша.
Объяснение кода: возвращает ассет меша со значениями из меша в форме сердца.
Сохраните файл и вернитесь в 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() добавьте следующее:
Перейдите в CurveType1() и добавьте следующее:
Чтобы увидеть подробные результаты для разных типов кривых, введите значения в соответствии со скриншотом:
Для списка Curve Type выберите значение Curve1, убедитесь, что для Edit Type выбрано None и нажмите Play. Вы должны увидеть, что меш расходится в паттерн. Покрутите модель, чтобы увидеть её в виде сбоку, и сравните результаты для обоих типов кривых. Здесь вы видите, как выбранный Curve Type влияет на смещение меша.
Вот и всё! Можете нажать на Clear Selected Vertices, чтобы сбросить Selected Indices и поэкспериментировать с собственными паттернами. Но не забывайте, что есть и другие факторы, которые будут влиять на конечный результат меша, а именно:
Куда двигаться дальше?
Файлы готового проекта находятся в архиве проекта туториала.
Не останавливайтесь на этом! Попробуйте использовать более сложные техники, применяемые в туториале «Процедурная генерация лабиринтов в Unity».
Надеюсь, вам понравился этот туториал, а информация оказалась полезной. Особую благодарность я выражаю Джасперу Флику из Catlike Coding за его отличные туториалы, которые помогли мне собрать демо для моего проекта.
Шейдеры интерактивных карт в Unity
Этот туториал посвящён интерактивным картам и их созданию в Unity при помощи шейдеров.
Этот эффект может служить основой более сложных техник, например голографических проекций или даже песочного стола из фильма «Чёрная пантера».
Источником вдохновения для этого туториала стал опубликованный Baran Kahyaoglu твит, демонстрирующий пример того, что он создаёт для Mapbox.
Nothing really special this time, no shader/vfx magic, it’s just an interactive map (with pan&zoom) on the same table/environment.
It uses new #unity HDRP though so it looks very cool compared to regular boring topdown maps.#gamedev #madewithunity #builtwithmapbox #map pic.twitter.com/hUgZqfloUK
and even though there’s nothing special about it, it’s so much fun to move around because it just looks good.
It was hard to record in HD because of the mouse movement but here’s a low quality one. pic.twitter.com/ileBzYwHO9
Сцена (за исключением карты) взята из демо Unity Visual Effect Graph Spaceship (см. ниже), которое можно скачать здесь.
Часть 1. Смещение вершин
Анатомия эффекта
Первое, что можно сразу заметить — географические карты плоски: если их использовать в качестве текстур, то им не хватает трёхмерности, которую бы имела настоящая 3D-модель соответствующей области карты.
Можно применить такое решение: создать 3D-модель той области, которая нужна в игре, а затем наложить на неё текстуру из географической карты. Это поможет решить задачу, но требует много времени и не позволит реализовать эффект «прокрутки» из видео Baran Kahyaoglu.
Очевидно, что лучше всего применить более технический подход. К счастью, для изменения геометрии 3D-модели можно использовать шейдеры. С их помощью можно превратить любую плоскость в долины и горы нужной нам области.
В этом туториале мы используем карту коммуны Кильота в Чили, знаменитой своими характерными холмами. На изображении ниже показана текстура области, нанесённая на круглый меш.
Хоть мы и видим холмы и горы, они всё-таки совершенно плоские. Это разрушает иллюзию реализма.
Экструдирование нормалей
Первым шагом к использованию шейдеров для изменения геометрии является техника под названием «экструдирование нормалей» (normal extrusion). Ей требуется модификатор вершин: функция, способная манипулировать отдельными вершинами 3D-модели.
Способ применения модификатора вершин зависит от типа используемого шейдера. В этом туториале мы будем изменять Surface Standard Shader — один из типов шейдеров, которые можно создавать в Unity.
Существует множество способов манипуляции вершинами 3D-модели. Один из самых первых способов, описываемых в большинстве туториалов по вершинным шейдерам — это экструдирование нормалей. Он заключается в выталкивании каждой вершины «наружу» (экструдировании), что придаёт 3D-модели более раздутый вид. «Наружу» обозначает, что каждая вершина движется вдоль направления нормали.
Для гладких поверхностей это срабатывает очень хорошо, но в моделях с плохим соединением вершин такой способ может создавать странные артефакты. Этот эффект хорошо объяснён в одном из моих первых туториалов: A Gentle Introduction to Shaders, где я показал, как экструдировать и интрудировать 3D-модель.
Отредактированный шейдер выглядит следующим образом:
Экструдирование нормалей с текстурами
Использованный нами выше код работает правильно, но он далёк от того эффекта, которого мы хотим достичь. Причина заключается в том, что мы не хотим экструдировать все вершины на одинаковую величину. Мы хотим, чтобы поверхность 3D-модели соответствовала долинам и горам соответствующего географического региона. Сначала нам каким-то образом нужно хранить и извлекать информацию о том, насколько поднята каждая точка карты. Мы хотим, чтобы на экструдирование влияла текстура, в которой закодированы высоты ландшафта. Такие текстуры часто называют картами вершин (heightmaps), однако нередко они также называются картами глубин (depthmaps), в зависимости от контекста. Получив информацию о высотах, мы сможем модифицировать экструдирование плоскости на основании карты высот. Как показано на схеме, это позволит нам контролировать поднятием и опусканием областей.
Довольно просто найти спутниковое изображение интересующей вас географической области и связанную с ней карту высот. Ниже показана спутниковая карта Марса (сверху) и карта высот (снизу), которые использовались в этом туториале:
Я подробно рассказывал о концепции карты глубин в ещё одной серии туториалов под названием «3D-фотографии Facebook изнутри: шейдеры параллакса» [перевод на Хабре].
В показанном ниже фрагменте кода текстура под названием _HeightMap используется для модификации величины экструдирования, выполняемого для каждой вершины:
И в самом деле, tex2D используется для сэмплирования пикселей из текстуры, вне зависимости от того, что в ней хранится, цвета или высоты. Однако можно заметить, что tex2D невозможно использовать в вершинной функции.
Причина в том, что tex2D не только считывает пиксели из текстуры. Она также решает, какую версию текстур использовать, в зависимости от расстояния до камеры. Эта техника называется MIP-текстурированием (mipmapping): она позволяет иметь уменьшенные версии одной текстуры, которые можно автоматически использовать на различных расстояниях.
В поверхностной функции шейдер уже знает, какую MIP-текстуру использовать. Эта информация может быть ещё не доступна в вершинной функции, и поэтому tex2D нельзя использовать с полной уверенностью. В отличие от неё, функции tex2Dlod можно передать два дополнительных параметра, которые в этом туториале могут иметь нулевое значение.
Результат чётко заметен на изображениях ниже
В данном случае можно выполнить одно небольшое упрощение. Код, который мы рассматривали ранее, может работать с любой геометрией. Однако мы можем допустить, что поверхность абсолютно плоская. На самом деле мы действительно хотим применить этот эффект к плоскости.
Следовательно, можно удалить v.normal и заменить её на float3(0, 1, 0) :
Мы могли это сделать, потому что все координаты в appdata_base хранятся в пространстве модели, то есть они задаются относительно центра и ориентации 3D-модели. Перенос, поворот и масштабирование при помощи transform в Unity меняют позицию, поворот и масштаб объекта, но не влияют на исходную 3D-модель.
Часть 2. Эффект прокрутки
Всё, что мы сделали выше, довольно неплохо работает. Прежде чем продолжить, вынесем код, необходимый для вычисления новой высоты вершины, в отдельную функцию getVertex :
Тогда вся функция vert будет иметь вид:
Мы сделали так потому, что в ниже нам понадобится вычислять высоту нескольких точек. Благодаря тому, что эта функциональность будет в собственной отдельной функции, код станет намного проще.
Вычисление UV-координат
Однако это приводит нас к другой проблеме. Функция getVertex зависит не только от позиции текущей вершины (v.vertex), но и от её UV-координат ( v.texcoord ).
Это означает, что имеющаяся система способна вычислять смещение высоты только для текущей вершины. Такое ограничение не позволит нам двигаться дальше, поэтому нужно найти решение.
Проще всего будет найти способ вычисления UV-координат 3D-объекта, зная позицию его вершины. Это очень сложная задача, и существует несколько техник её решения (одна из самых популярных — это трипланарная проекция). Но в данном конкретном случае нам не нужно сопоставлять UV с геометрией. Если мы допустим, что шейдер всегда будет применяться к плоскому мешу, то задача становится тривиальной.
Мы можем вычислять UV-координаты (нижнее изображение) из позиций вершин (верхнее изображение) благодаря тому, что на плоском меше и те, и другие накладываются линейно.
Это значит, что для решения нашей задачи нам нужно преобразовать компоненты XZ позиции вершины в соответствующие UV-координаты.
Такая процедура называется линейной интерполяцией. Она подробно рассмотрена на моём веб-сайте (например: The Secrets Of Colour Interpolation).
В большинстве случаев значения UV находятся в интервале от до
; координаты каждой вершины, напротив, потенциально ничем не ограничены. С точки зрения математики, для преобразования из XZ в UV нам нужны только их предельные значения:
Эти значения изменяются в зависимости от используемого меша. На плоскости Unity UV-координаты находятся в интервале от до
, а координаты вершин находятся в интервале от
до
.
Уравнения преобразования XZ в UV имеют вид:
Если вам незнакомо понятие линейной интерполяции, то эти уравнения могут показаться довольно пугающими.
Однако выводятся они достаточно просто. Давайте рассмотрим только пример . У нас есть два интервала: один имеет значения от
до
, другой — от
до
. Входящими данными для координаты
является координата текущей обрабатываемой вершины, а выходными данными будет координата
, используемая для сэмплирования текстуры.
Нам необходимо сохранить свойства пропорциональности между и его интервалом, и
и его интервалом. Например, если
имеет значение 25% от его интервала, то
тоже будет иметь значение 25% от его интервала.
Всё это показано на следующей схеме:
Из этого мы можем вывести, что пропорция, составляемая красным отрезком по отношению к розовому, должна быть такой же, что и пропорция между синим отрезком и голубым:
Теперь мы можем преобразовать показанное выше уравнение, чтобы получить :
Эти уравнения можно реализовать в коде следующим образом:
Теперь мы можем вызывать функцию getVertex без необходимости передачи ей v.texcoord :
Тогда вся функция vert принимает вид:
Эффект прокрутки
Благодаря написанному нами коду на меше отображается вся карта. Если мы хотим усовершенствовать отображение, то нужно внести изменения.
Давайте ещё немного формализуем код. Во-первых, нам может понадобиться приблизить отдельную часть карты, а не смотреть на неё целиком.
Эту область можно определить двумя значениями: её размерами ( _CropSize ) и расположением на карте ( _CropOffset ), измеряемыми в пространстве вершин (от _VertexMin до _VertexMax ).
Получив эти два значения, мы можем ещё раз использовать линейную интерполяцию, чтобы getVertex вызывалась не для настоящей позиции вершины 3D-модели, а для отмасштабированной и перенесённой точки.
Если мы хотим, чтобы выполнялась прокрутка, то достаточно будет обновлять _CropOffset через скрипт. Благодаря этому область усечения будет двигаться, фактически выполняя прокрутку по ландшафту.
Чтобы это сработало, очень важно указать для режима Wrap Mode всех текстур значение Repeat. Если этого не сделать, то мы не сможем зацикливать текстуру.
Часть 3. Затенение рельефа
Плоское затенение
Весь написанный нами код работает, но имеет серьёзную проблему. Затенение модели выполняется как-то странно. Поверхность правильно искривляется, но реагирует на свет так, как будто является плоской.
Это очень чётко видно на показанных ниже изображениях. На верхнем изображении показан имеющийся шейдер; на нижнем показано, как он работает на самом деле.
Устранение этой проблемы может быть большой сложностью. Но сначала нам нужно разобраться, в чём же ошибка.
Операция экструдирования нормалей изменила общую геометрию плоскости, которую мы использовали изначально. Однако Unity изменила только позицию вершин, но не их направления нормалей. Направление нормали вершины, как понятно из названия, — это вектор единичной длины (направление), указывающий перпендикулярно поверхности. Нормали необходимы, потому что они играют важную роль в затенении 3D-модели. Они используются всеми поверхностными шейдерами для вычисления того, как свет должен отражаться от каждого треугольника 3D-модели. Обычно это нужно для улучшения трёхмерности модели, например, это заставляет свет отражаться от плоской поверхности так же, как он отражался бы от изогнутой. Этот трюк часто используется, чтобы низкополигональные поверхности выглядели более плавными, чем есть на самом деле (см. ниже).
Однако в нашем случае происходит обратное. Геометрия искривлённая и плавная, но так как все нормали направлены вверх, свет отражается от модели так, как будто она плоская (см. ниже):
Подробнее о роли нормалей в затенении объекта можно прочитать в статье о Normal Mapping (Bump Mapping), где одинаковые цилиндры выглядят очень разными, несмотря на одну 3D-модель, из-за разных способов вычислений нормалей вершин (см. ниже).
К сожалению, ни в Unity, ни в языке создания шейдеров нет встроенного решения для автоматического пересчёта нормалей. Это значит, что придётся изменять их вручную в зависимости от локальной геометрии 3D модели.
Вычисление нормалей
Единственный способ устранения проблемы с затенением — это вычисление нормалей вручную на основании геометрии поверхности. Подобная задача рассматривалась в посте Vertex Displacement – Melting Shader Part 1, где она использовалась для симуляции таяния 3D-моделей в игре Cone Wars.
Хотя готовый код должен будет работать в 3D-координатах, давайте пока ограничим задачу только двумя измерениями. Представим, что на нужно вычислить направление нормали, соответствующей точке на 2D-кривой (большая синяя стрелка на схеме ниже).
С геометрической точки зрения, направление нормали (большая синяя стрелка) — это вектор, перпендикулярный касательной, проходящей через интересующую нас точку (тонкую синюю линию). Касательную можно представить как линию, расположенную на кривизне модели. Касательный вектор — это единичный вектор, который лежит на касательной.
Это значит, что для вычисления нормали нужно сделать два шага: сначала найти прямую, касательную к нужной точке; затем вычислить вектор, перпендикулярный ей (который и будет необходимым направлением нормали).
Вычисление касательных
Для получения нормали нам сначала нужно вычислить касательную. Её можно аппроксимировать, сэмплировав точку поблизости и использовав его для построения отрезка рядом с вершиной. Чем меньше отрезок, тем точнее будет значение.
Необходимы три этапа:
Векторное произведение
Получив подходящие векторы касательной и касательной к двум точкам, мы можем вычислить нормаль при помощи операции под названием векторное произведение. Существует множество определений и объяснений векторного произведения и того, что оно делает.
Векторное произведение получает два вектора и возвращает один новый. Если два исходных вектора были единичными (их длина равна единице), и они расположены под углом 90, то получившийся вектор будет расположен под 90 градусов относительно обоих.
Поначалу это может сбивать с толку, но графически это можно представить так: векторное произведение двух осей создаёт третью. То есть , но ещё и
, и так далее.
Если мы сделаем достаточно малый шаг (в коде это offset ), то векторы касательной и касательной к двум точкам будут находиться под углом 90 градусов. Вместе с вектором нормали они образуют три перпендикулярные оси, ориентированные вдоль поверхности модели.
Зная это, мы можем написать весь необходимый код для вычисления и обновления вектора нормали.
Соединяем всё вместе
Теперь, когда всё работает, мы можем вернуть и эффект прокрутки.
И на этом наш эффект наконец-то завершён.
Куда двигаться дальше
Этот туториал может стать основой более сложных эффектов, например, голографических проекций или даже копии песочного стола из фильма «Чёрная пантера».