Создание графика.

Введение.

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

Создаем линию кубов.

Хорошее понимание математики имеет важное значение при программировании. На самом фундаментальном уровне математика - это манипуляция символами, которые представляют числа. Решение уравнения сводится к переписыванию одного набора символов на другое, обычно более короткое. Правила математики диктуют, как это переписывание можно сделать.
Например, мы имеем функцию f (x) = x + 1. Мы можем подставить число для х, скажем 3. Это приводит к f (3) = 3 + 1 = 4. Мы предоставили 3 в качестве входных данных и в итоге получили 4. Можно сказать, что функция отображает 3 в 4. Более короткий способ записать это будет как пара вход-выход, как (3,4). Мы можем создать много пар вида (x, f (x)). Например, (5,6) и (8,9) и (1,2) и (6,7). Но легче понять функцию, когда мы упорядочить пары по входному числу. (1,2) и (2,3) и (3,4) и т. д.
Функцию f (x) = x + 1 легко понять. f (x) = (x-1) 4 + 5x3-8x2 + 3x сложнее. Мы могли бы записать несколько пар ввода-вывода, но это вряд ли даст нам хорошее представление об отображаемом ими сопоставлении. Нам понадобятся много пар рядом. Это закончится как море чисел, которые трудно разобрать. Вместо этого мы могли бы интерпретировать пары как двумерные координаты формы [x, f (x)]. Это 2D-вектор, где первое число представляет собой горизонтальную координату на оси X, а второе число представляет собой вертикальную координату по оси Y. Другими словами, y = f (x). Мы можем построить эти точки на поверхности. Если мы используем достаточно точек, мы получим линию. Результатом является график.
График может быстро дать нам представление о том, как работает функция. Это удобный инструмент, поэтому давайте создадим его в Unity. Начните с новой сцены через File / New Scene для этой цели или используйте сцену по умолчанию для нового проекта.

Префабы.

Графики создаются путем размещения точек в соответствующих координатах. Для этого нам нужна 3D-визуализация точки. Мы просто используем объект кубика Unity. Добавьте один на сцену и удалите его компонент коллайдера, так как мы не будем использовать физику.
Являются ли кубы лучшим способом визуализации графиков?read
Мы будем использовать скрипт для создания множества экземпляров этого куба и правильного размещения их. Для этого мы будем использовать куб в качестве шаблона. Перетащите куб из окна иерархии в окно проекта. Это создаст новый актив с пиктограммой синего куба, известной как префаб. Это готовый игровой объект, который существует в проекте, но не в сцене. 
Prefabs - удобный способ настройки игровых объектов. Если вы измените префаб, все экземпляры его в любой сцене будут изменены одинаково. Например, изменение масштаба префаба также изменит масштаб куба, который все еще находится на сцене. Однако каждый экземпляр использует свою собственную позицию и поворот. Кроме того, игровые объекты могут изменять свои свойства, что превалирует над значениями префаба. Если сделаны большие изменения, такие как добавление или удаление компонента, связь между префабом и экземпляром будет нарушена.
Мы собираемся использовать скрипт для создания экземпляров префаба, поэтому нам больше не нужен экземпляр куба, который сейчас находится на сцене. Так что удалите его.

Компонент графика.

Нам нужен скрипт C# для генерации нашего графика. Создайте его и назовите его. Начнем с простого класса, который расширяет MonoBehaviour, поэтому его можно использовать как компонент для игровых объектов. Дайте ему общедоступное поле, чтобы содержать ссылку на префаб для создания точек с именем pointPrefab. Поскольку нам нужно получить доступ к компоненту Transform, чтобы расположить точки, введите тип поля.
using UnityEngine;

public class Graph : MonoBehaviour {

 public Transform pointPrefab;
}
Добавьте пустой объект игры в сцену, через GameObject / Create Empty, поместите его в начало координат и назовите его Graph. Добавьте наш компонент Graph к этому объекту, перетащив его или через кнопку Add Component. Затем перетащите наш префаб в поле Prefab Point графика. Теперь он содержит ссылку на компонент Transform префаба.

Инстанцирование префаба.

Создание экземпляра игрового объекта выполняется с помощью метода Instantiate. Это общедоступный метод типа объекта Unity, который Graph косвенно наследует путем расширения MonoBehaviour. Метод Instantiate клонирует любой объект Unity переданный ему в качестве аргумента. В случае префаба это приведет к добавлению экземпляра этого префаба в текущую сцену. Давайте сделаем это, когда наш компонент Graph пробудится.
public class Graph : MonoBehaviour {

 public Transform pointPrefab;
 
 void Awake () {
  Instantiate(pointPrefab);
 }
}
В этот момент при входе в режим воспроизведения будет создан один куб в начале координат, при условии, что позиция префаба установлена равной нулю. Чтобы поместить точку в другое место, нам нужно отрегулировать положение экземпляра. Метод Instantiate дает нам ссылку на то, что он создал. Поскольку мы дали ссылку на компонент Transform, это то, что мы получаем взамен. Давайте отследим его с переменной.
void Awake () {
  Transform point = Instantiate(pointPrefab);
 }
Теперь мы можем отрегулировать положение точки, назначив ему трехмерный вектор. Как мы скорректировали локальное вращение ручек часов в предыдущем уроке, мы отрегулируем локальное положение точки через свойство localPosition, а не position.
3D-векторы создаются с помощью структуры Vector3. Поскольку это структура, она действует как значение, похожее на число, а не на объект. Например, зададим координату X нашей точки равной 1, оставив ее координаты Y и Z в нуле. Вектор 3 имеет для этого свойство right.
Transform point = Instantiate(pointPrefab);
  point.localPosition = Vector3.right;
Не следует ли капитализировать свойства? read
При входе в режим воспроизведения мы все равно получаем один куб, только в несколько иной позиции. Давайте создадим второй экземпляр и поместим его на дополнительный шаг вправо. Это можно сделать, умножив вектор right на 2. Повторите код создания и позиционирования, затем добавьте умножение в новый код.
void Awake () {
  Transform point = Instantiate(pointPrefab);
  point.localPosition = Vector3.right;

  Transform point = Instantiate(pointPrefab);
  point.localPosition = Vector3.right * 2f;
 }
Можно ли умножать структуры и числа?read
Этот код приведет к ошибке компиляции, поскольку мы пытаемся дважды определить переменную точки. Если мы хотим использовать другую переменную, мы должны дать ей другое имя. Либо повторно использовать переменную, которую мы уже имеем, не определяя ее. Так как нам не нужно держать ссылку на первую точку, просто назначьте новую точку той же переменной.
Transform point = Instantiate(pointPrefab);
  point.localPosition = Vector3.right;

//  Transform point = Instantiate(pointPrefab);
  point = Instantiate(pointPrefab);
  point.localPosition = Vector3.right * 2f;

Циклы.

Давайте создадим больше точек, пока у нас не будет десяти. Мы могли бы повторить один и тот же код еще восемь раз, но это очень неэффективное программирование. В идеале мы пишем только код для одной точки и инструктируем программу выполнять ее несколько раз с небольшими вариациями.
Оператор while может использоваться для повторения кода. Примените его к первым двум строкам нашего метода и удалите другие строки.
void Awake () {
  while {
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right;
  }
//  point = Instantiate(pointPrefab);
//  point.localPosition = Vector3.right * 2f;
 }
Подобно оператору if, за while должно следовать выражение в круглых скобках. Как и с if, следующий код будет выполняться только тогда, когда выражение оценивается как true. После этого программа вернется к инструкции while. Если в этот момент выражение снова будет считаться истинным, блок кода будет выполнен снова. Это повторяется до тех пор, пока выражение не примет значение false.
Поэтому мы должны добавить выражение после этого. Мы должны быть осторожны, чтобы убедиться, что цикл не повторяется навсегда. Бесконечные циклы заставляют программы застревать, требуя ручного завершения пользователем. Самое безопасное выражение, которое компилируется, просто false.
while (false) {
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right;
  }
Можем ли мы определить точку внутри цикла? read
Ограничение цикла может быть выполнено путем отслеживания того, сколько раз мы повторяли код. Мы можем использовать целочисленную переменную, чтобы отслеживать это. Она будет содержать номер итерации цикла, поэтому давайте назовем его i. Чтобы иметь возможность использовать его в выражении while, он должен быть определен ранее.
void Awake () {
  int i;
  while (false) {
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right;
  }
 }
Каждая итерация увеличивает число на единицу.
int i;
  while (false) {
   i = i + 1;
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right;
  }
Это приводит к ошибке компиляции, потому что мы пытаемся использовать i до того, как мы присвоили ей значение. Мы должны явно присваивать ноль i, когда мы определяем его.
int i = 0;
Теперь i становится 1 в начале первой итерации, 2 в начале второй итерации и т. д. Но выражение while оценивается перед каждой итерацией. Итак, прямо перед первой итерацией i равна нулю, она равна 1 до второй и т. д. Итак, после десятой итерации i равно десять. На этом этапе мы хотим остановить цикл, поэтому его выражение должно оцениваться как false. Другими словами, мы должны продолжать, пока мне меньше десяти. Математически это выражается как i < 10 В коде это записывается также оператором "меньше".
  int i = 0;
  while (i < 10) {
   i = i + 1;
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right;
  }
Теперь мы получим десять кубов после входа в режим воспроизведения. Но все они оказываются в том же положении. Чтобы поместить их в ряд вдоль оси X, умножьте вектор right на i.
point.localPosition = Vector3.right * i;
Обратите внимание, что в настоящее время первый куб заканчивается координатой X 1, а последний куб заканчивается на 10. В идеале мы начинаем с 0, позиционируя первый куб в начале координат. Мы можем сдвинуть все точки на одну единицу влево, умножив их справа на (i - 1) вместо i. Однако мы могли бы пропустить это дополнительное вычитание, увеличив i в конце блока, а не в начале.
while (i < 10) {
//   i = i + 1;
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right * i;
   i = i + 1;
  }

Краткий синтаксис.

Так как цикл, повторяемый определенное количество раз настолько распространена, удобно использовать как можно более короткий синтаксис. Некоторые синтаксические сахара могут помочь нам в этом.
Во-первых, давайте рассмотрим увеличение итерационного числа. Когда выполняется операция вида x = x * y, ее можно укоротить до x * = y. Это работает для всех операторов, работающих на двух операндах того же типа.
//   i = i + 1;
   i += 1;
Идя еще дальше, при увеличении или уменьшении числа на 1 это можно сократить до ++ x или --x.
//   i += 1;
   ++i;
Одним из свойств операторов присваивания является то, что они также могут использоваться в качестве выражений. Это означает, что вы можете написать что-то вроде y = (x + = 3). Это увеличит x на три и присвоит результат этого y. Это говорит о том, что мы можем увеличивать i внутри выражения while, сокращая блок кода.
while (++i < 10) {
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right * i;
//   ++i;
  }
Однако теперь мы увеличиваем i перед сравнением, а не потом, что приведет к еще одной итерации. В частности, для ситуаций, подобных этой, операторы увеличения и уменьшения на 1 (инкремента и декремента) также могут быть помещены после переменной, а не перед ней. Результатом этого выражения является исходное значение, прежде чем оно будет изменено.
//  while (++i < 10) {
  while (i++ < 10) {
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right * i;
  }
Хотя оператор while работает для всех типов циклов, существует альтернативный синтаксис, особенно подходящий для итерации по диапазонам. Это цикл for. Он работает как while, за исключением того, что как объявление переменной итератора, так и его сравнение содержатся в круглых скобках, разделенных точкой с запятой.
//  int i = 0;
//  while (i++ < 10) {
  for (int i = 0; i++ < 10) {
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right * i;
  }
Это приведет к ошибке компиляции, потому что на самом деле есть три части. Третий - для увеличения итератора, отделяя его от сравнения
//  for (int i = 0; i++ < 10) {
  for (int i = 0; i < 10; i++) {
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right * i;
  }
Зачем использовать i ++, а не ++ i в цикле for? read
Классический вид для цикла такой (int i = 0; i < someLimit; i++). Вы столкнетесь с этим фрагментом кода во множестве программ и скриптов.

Изменение диапазона.

В настоящее время нашим кубам присваиваются координаты X от 0 до 9. Это не удобный диапазон при работе с функциями. Часто для X используется диапазон 0-1. Или при работе с функциями, центрированными вокруг нуля, диапазон -1-1. Давайте переместим наши кубы соответственно.
Позиционирование наших десяти кубов вдоль линейного сегмента на два блока приведет к их перекрытию. Чтобы предотвратить это, мы уменьшим их масштаб. По умолчанию каждый куб имеет размер 1 в каждом измерении, поэтому для их соответствия мы должны уменьшить их масштаб до 2/10 = 1/5. Мы можем сделать это, установив локальный масштаб каждой точки в свойство Vector3.one, разделенное на пять.
for (int i = 0; i < 10; i++) {
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right * i;
   point.localScale = Vector3.one / 5f;
  }
Чтобы снова собрать кубы, разделите их позиции на пять.
point.localPosition = Vector3.right * i / 5f;
Это заставляет их покрывать диапазон 0-2. Чтобы превратить это в диапазон -1-1, вычтите 1 перед масштабированием вектора.
point.localPosition = Vector3.right * (i / 5f - 1f);
Теперь первый куб имеет координату X равную -1, а последний имеет координату 0,8. Однако размер куба равен 0,2. Когда куб центрирован по своему положению, левая часть первого куба находится на -1,1, а правая часть последнего куба - 0,9. Чтобы аккуратно заполнить диапазон -1-1 нашими кубами, мы должны перенести их на половину куба вправо. Это можно сделать, добавив 0.5 к i, прежде чем делить его.
point.localPosition = Vector3.right * ((i + 0.5f) / 5f - 1f);

Выносим расчеты из цикла.

Хотя все кубы имеют одинаковый масштаб, мы вычисляем его на каждой итерации цикла. Мы не должны этого делать. Вместо этого мы могли бы вычислить его один раз перед циклом, сохранить его в переменной Vector3 и использовать его в цикле.
void Awake () {
  Vector3 scale = Vector3.one / 5f;
  for (int i = 0; i < 10; i++) {
   Transform point = Instantiate(pointPrefab);
   point.localPosition = Vector3.right * ((i + 0.5f) / 5f - 1f);
   point.localScale = scale;
  }
 }
Мы могли бы также определить переменную для позиции перед циклом. Когда мы создаем линию вдоль оси X, нам нужно только отрегулировать координату X положения внутри цикла. Поэтому нам больше не нужно умножать Vector3.right.
Vector3 scale = Vector3.one / 5f;
  Vector3 position;
  for (int i = 0; i < 10; i++) {
   Transform point = Instantiate(pointPrefab);
//   point.localPosition = Vector3.right * ((i + 0.5f) / 5f - 1f);
   position.x = (i + 0.5f) / 5f - 1f;
   point.localPosition = position;
   point.localScale = scale;
  }
Можем ли мы изменить компоненты вектора по отдельности? read
Это приведет к ошибке компиляции, жалуясь на использование неназначенной переменной. Это происходит потому, что мы присваиваем позицию чему-то, у чего мы пока не установили координаты Y и Z. Явно установим их перед циклом.
Vector3 position;
  position.y = 0f;
  position.z = 0f;
  for (int i = 0; i < 10; i++) {
   …
  }

Используем x для получения y.

Идея состоит в том, что позиции наших кубов определяются как [x, f (x) 0], поэтому мы можем использовать их для отображения функции. На этом этапе координаты Y всегда равны нулю, что представляет собой тривиальную функцию f (x) = 0. Чтобы показать другую функцию, мы должны определить координату Y внутри цикла, а не перед ней. Давайте сделаем его равным X, представляя функцию f (x) = x.
Vector3 position;
//  position.y = 0f;
  position.z = 0f;
  for (int i = 0; i < 10; i++) {
   Transform point = Instantiate(pointPrefab);
   position.x = (i + 0.5f) / 5f - 1f;
   position.y = position.x;
   point.localPosition = position;
   point.localScale = scale;
  }
 
Несколько менее очевидной функцией будет f (x) = x2, которая определяет параболу с ее минимумом в нуле.
position.y = position.x * position.x;

Создаем больше кубов.

Хотя на данный момент у нас есть график функции, он уродлив. Поскольку мы используем только десять кубов, предлагаемая линия выглядит очень блочно и дискретно. Было бы лучше, если бы мы использовали больше маленьких кубов.

Переменная разрешения.

Вместо использования фиксированного количества кубов мы можем настроить его. Чтобы сделать это возможным, добавьте общедоступное целочисленное поле для разрешения в Graph. Дайте ему по умолчанию 10, что мы и сейчас используем.
public int resolution = 10;
Идея состоит в том, что мы можем отрегулировать разрешение графика, изменив это значение, которое может быть выполнено с помощью инспектора. Однако не все целые числа являются действительными разрешениями. Как минимум, они должны быть положительными. Мы можем поручить инспектору обеспечить соблюдение диапазона для нашей резолюции. Это делается путем записи диапазона между квадратными скобками перед определением поля.
[Range] public int resolution = 10;
Range - это тип атрибута, определенный Unity. Атрибут - это способ привязки метаданных к структурам кода, в данном случае - к полю. Инспектор Unity проверяет, имеет ли поле атрибут Range, прикрепленный к нему. Если это так, он будет использовать ползунок вместо поля ввода по умолчанию для чисел. Однако для этого ему необходимо знать допустимый диапазон. Поэтому Range имеет два параметра для минимального и максимального значения. Давайте использовать 10 и 100. Кроме того, атрибуты обычно записываются выше, а не перед тем, к чему они привязаны.
[Range(10, 100)]
 public int resolution = 10;
Означает ли это, что разрешение ограничено 10-100?read

Изменяем создание кубов.

Чтобы действительно использовать разрешение, мы должны изменить количество кубов, которые мы создаем. Вместо того, чтобы фиксировать определенное количество раз в Awake, количество итераций теперь ограничено разрешением. Поэтому, если разрешение установлено равным 50, мы создадим 50 кубов после входа в режим воспроизведения.
for (int i = 0; i < resolution; i++) {
   …
  }
Нам также нужно настроить масштаб и положение кубов, чтобы они находились внутри диапазона -1-1. Размер каждого шага, который мы должны сделать для каждой итерации, теперь составляет 2/resolution, а не всегда 1/5. Сохраните это значение в переменной и используйте его для вычисления масштаба кубов и их координат X.
void Awake () {
  float step = 2f / resolution;
  Vector3 scale = Vector3.one * step;
  Vector3 position;
  position.z = 0f;
  for (int i = 0; i < resolution; i++) {
   Transform point = Instantiate(pointPrefab);
   position.x = (i + 0.5f) * step - 1f;
   position.y = position.x * position.x;
   point.localPosition = position;
   point.localScale = scale;
  }
 }

Устанавливаем родителя.

После ввода режима с разрешением 50 на сцене появляется много экземпляров.
Эти кубы в настоящее время являются корневыми объектами, но для них имеет смысл быть дочерними элементами объекта графа. Мы можем установить это отношение после создания экземпляра куба, вызвав метод SetParent компонента Transform для куба. Мы должны предоставить ему компонент Transform нового родителя. Мы можем напрямую обращаться к компоненту Transform объекта графика через его свойство transform, которое Graph унаследовал.
for (int i = 0; i < resolution; i++) {
   Transform point = Instantiate(pointPrefab);
   position.x = (i + 0.5f) * step - 1f;
   position.y = position.x * position.x;
   point.localPosition = position;
   point.localScale = scale;
   point.SetParent(transform);
  }
Когда будет установлен новый родитель, Unity попытается сохранить объект в исходном мировом положении, вращении и масштабировании. В нашем случае нам это не нужно. Мы можем сообщить об этом, предоставив false в качестве второго аргумента SetParent.
point.SetParent(transform, false);

Раскрашиваем график.

На белый график не очень приятно смотреть. Мы могли бы использовать другой сплошной цвет, но это тоже не очень интересно. То, что мы можем сделать, это использовать позицию точки, чтобы определить ее цвет.
Прямым способом настройки цвета каждого куба было бы установить свойство цвета его материала. Мы можем сделать это в цикле. Поскольку каждый куб получит другой цвет, это означает, что мы получим один уникальный материал для каждого объекта. Хотя это работает, оно не очень эффективно. Было бы намного проще, если бы мы могли использовать один материал, который напрямую использует положение в качестве его цвета. К сожалению, Unity не имеет такого материала. Итак, давайте сделаем наш собственный.

Создаем свой шейдер.

GPU запускает шейдерные программы для рендеринга трехмерных объектов. Ассеты материалов Unity определяют, какой шейдер используется, и позволяет настроить его свойства. Нам нужно создать собственный шейдер, чтобы получить нужную функциональность. Создайте его через Assets / Create / Shader / Standard Surface Shader и назовите его ColoredPoint.
Теперь у вас есть объект shader, который вы можете открыть, как скрипт. Наш шейдерный файл содержит код для определения поверхностного шейдера, который использует другой синтаксис, чем C#. Ниже приведено содержимое файла, причем все строки комментариев удалены для краткости.
Shader "Custom/ColoredPoint" {
 Properties {
  _Color ("Color", Color) = (1,1,1,1)
  _MainTex ("Albedo (RGB)", 2D) = "white" {}
  _Glossiness ("Smoothness", Range(0,1)) = 0.5
  _Metallic ("Metallic", Range(0,1)) = 0.0
 }
 SubShader {
  Tags { "RenderType"="Opaque" }
  LOD 200
  
  CGPROGRAM
  #pragma surface surf Standard fullforwardshadows

  #pragma target 3.0

  sampler2D _MainTex;

  struct Input {
   float2 uv_MainTex;
  };

  half _Glossiness;
  half _Metallic;
  fixed4 _Color;

  UNITY_INSTANCING_CBUFFER_START(Props)
  UNITY_INSTANCING_CBUFFER_END

  void surf (Input IN, inout SurfaceOutputStandard o) {
   fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
   o.Albedo = c.rgb;
   o.Metallic = _Metallic;
   o.Smoothness = _Glossiness;
   o.Alpha = c.a;
  }
  ENDCG
 }
 FallBack "Diffuse"
}
Как работают поверхностные шейдеры? read
Наш новый шейдер обладает свойствами для сплошного цвета, текстуры, а также глянцевой и металлической поверхности. Поскольку мы будем основывать цвет на позиции точки, нам не нужен сплошной цвет или текстура. В приведенном ниже коде все ненужные части удаляются, оставляя альбедо сплошным черным и используя альфа 1.
Shader "Custom/ColoredPoint" {
 Properties {
//  _Color ("Color", Color) = (1,1,1,1)
//  _MainTex ("Albedo (RGB)", 2D) = "white" {}
  _Glossiness ("Smoothness", Range(0,1)) = 0.5
  _Metallic ("Metallic", Range(0,1)) = 0.0
 }
 SubShader {
  Tags { "RenderType"="Opaque" }
  LOD 200
  
  CGPROGRAM
  #pragma surface surf Standard fullforwardshadows

  #pragma target 3.0

//  sampler2D _MainTex;

  struct Input {
//   float2 uv_MainTex;
  };

  half _Glossiness;
  half _Metallic;
//  fixed4 _Color;

  UNITY_INSTANCING_CBUFFER_START(Props)
  UNITY_INSTANCING_CBUFFER_END

  void surf (Input IN, inout SurfaceOutputStandard o) {
//   fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
//   o.Albedo = c.rgb;
   o.Metallic = _Metallic;
   o.Smoothness = _Glossiness;
//   o.Alpha = c.a;
   o.Alpha = 1;
  }
  ENDCG
 }
 FallBack "Diffuse"
}
Что такое альбедо и альфа? read
На данный момент шейдер не компилируется, поскольку поверхностные шейдеры не могут работать с пустой структурой входа. Здесь мы определяем, какие пользовательские данные необходимы для цветных пикселей. В нашем случае нам нужно положение точки. Мы можем получить доступ к мировой позиции, добавив float3 worldPos; на вход.
struct Input {
   float3 worldPos;
  };
Означает ли это, что перемещение графика повлияет на его цвет?read
Теперь, когда у нас есть работающий шейдер, создайте для него материал, названный ColoredPoint. Установите его для использования нашего шейдера, выбрав «Custom / Colored Point» в раскрывающемся списке «Shader».
Попросите наш префаб куба использовать этот материал вместо стандартного. Вы можете сделать это, перетащив материал непосредственно на префаб.

Цвет на основе позиции.

При входе в режим воспроизведения наш график теперь будет создавать черные кубы. Чтобы дать им цвет, нам нужно изменить o.Albedo. Вместо сохранения значения по умолчанию, равного нулю, сделайте его красную компоненту равной координате X.
                        o.Albedo.r = IN.worldPos.x;
   o.Metallic = _Metallic; 
Разве мы не должны полностью инициализировать o.Albedo? read
Кубы с положительными координатами X теперь становятся все более красными. Те, у кого отрицательные координаты Х, остаются черными, потому что цвета не могут быть отрицательными. Чтобы получить красный переход от -1 до 1, мы должны вдвое уменьшить координаты X и добавить 0.5.
o.Albedo.r = IN.worldPos.x * 0.5 + 0.5;
Давайте будем использовать координату Y для компонента зеленого цвета, точно так же, как X. В шейдере мы можем сделать это в одной строке с помощью IN.worldPos.xy и назначить o.Albedo.rg.
o.Albedo.rg = IN.worldPos.xy * 0.5 + 0.5;
Красный плюс зеленый становится желтым, поэтому наш график в настоящее время идет от светло-зеленого до желтого. Если бы координаты Y начались с -1, мы бы получили темно-зеленые цвета. Чтобы увидеть это, измените код в Graph.Awake, чтобы он отображал функцию f (x) = x3.
position.y = position.x * position.x * position.x;

Анимируем график.

Отображение статического графика полезно, но интереснее смотреть на движущийся график. Итак, добавим поддержку для анимации функций. Это делается путем включения времени в качестве дополнительного параметра функции, используя функции формы f (x, t) вместо просто f (x), где t - время.

Отслеживаем трек точек.

Чтобы анимировать график, нам нужно будет регулировать его точки с течением времени. Мы могли бы сделать это, удалив все точки и создав новые, обновленные. Но это неэффективный способ сделать это. Гораздо лучше использовать одни и те же точки, каждый раз изменяя их позиции. Чтобы сделать это легко, мы будем использовать поле, чтобы сохранить ссылку на все наши точки. Добавьте поле точек в Graph типа Transform.
Transform points;
Это поле позволяет нам ссылаться на одну точку, но нам нужен доступ ко всем из них. Чтобы сделать это возможным, нам нужно использовать массив точек. Мы можем превратить наше поле в массив, поставив пустые квадратные скобки за его типом.
Transform[] points;
Поле points теперь может ссылаться на массив, элементы которого имеют тип Transform. Массивы - это объекты, а не простые значения. Мы должны явно создать такой объект и сделать наше поле ссылкой на него. Это делается путем записи new, за которым следует тип массива, т.е. new Transform [] в нашем случае. Создайте массив в Awake, перед нашим циклом и назначьте его points.
points = new Transform[];
  for (int i = 0; i < resolution; i++) {
   …
  }
При создании массива вы должны указать его размер. Это определяет, сколько элементов он имеет, и не может быть изменено после его создания. Эта длина записывается внутри квадратных скобок при построении массива. В нашем случае его длина равна разрешению.
points = new Transform[resolution];
Теперь мы можем заполнить массив ссылками на наши точки. Доступ к элементу массива выполняется путем записи его индекса между квадратными скобками за полем массива или переменной. Индексы массива начинаются с нуля для первого элемента, как и счетчик итераций нашего цикла. Поэтому мы можем использовать его для доступа к соответствующему элементу массива.
points = new Transform[resolution];
  for (int i = 0; i < resolution; i++) {
   …
   points[i] = point;
  }
Теперь мы проходим через наш массив точек. Поскольку длина массива такая же, как и разрешение, мы можем также использовать это, чтобы ограничить наш цикл. Каждый массив имеет свойство Length для этой цели, поэтому давайте использовать его.
points = new Transform[resolution];
  for (int i = 0; i < points.Length; i++) {
   …
   points[i] = point;
  }

Обновление точек.

Чтобы на самом деле анимировать график, нам нужно установить координату Y точек в методе обновления компонента. Поэтому нам больше не нужно вычислять их в Awake, хотя нам все еще нужно использовать некоторое явное значение, которое должно быть равно нулю.
position.y = 0f;
  position.z = 0f;
  points = new Transform[resolution];
  for (int i = 0; i < points.Length; i++) {
   Transform point = Instantiate(pointPrefab);
   position.x = (i + 0.5f) * step - 1f;
//   position.y = position.x * position.x * position.x;
   point.localPosition = position;
   point.localScale = scale;
   point.SetParent(transform, false);
   points[i] = point;
  }
Добавьте метод Update с циклом for, как и Awake, но без какого-либо кода в его блоке.
void Update () {
  for (int i = 0; i < points.Length; i++) {}
 }
Каждая итерация начинается с получения ссылки на текущий элемент массива. Затем мы извлекаем позицию этой точки.
for (int i = 0; i < points.Length; i++) {
   Transform point = points[i];
   Vector3 position = point.localPosition;
  }
Теперь мы можем получить координату Y позиции, как мы это делали ранее.
for (int i = 0; i < points.Length; i++) {
   Transform point = points[i];
   Vector3 position = point.localPosition;
   position.y = position.x * position.x * position.x;
  }
Поскольку векторы не являются объектами, мы только скорректировали локальную переменную. Чтобы применить ее к точке, мы должны снова установить ее положение.
for (int i = 0; i < points.Length; i++) {
   Transform point = points[i];
   Vector3 position = point.localPosition;
   position.y = position.x * position.x * position.x;
   point.localPosition = position;
  }
Не могли ли мы напрямую назначить point.localPosition.y? read

Показываем волну синуса.

С этого момента, в режиме воспроизведения, точки нашего графика располагаются в каждом кадре. Мы этого не замечаем, потому что они всегда оказываются на тех же позициях. Мы должны включить время в функцию, чтобы изменить ее. Однако простое добавление времени вызовет повышение функции и быстрое исчезновение из вида. Чтобы этого не произошло, мы должны использовать функцию, которая изменяется, но остается в пределах фиксированного диапазона. Функция синуса идеально подходит для этого, поэтому мы будем использовать f (x) = sin (x). Мы можем использовать функцию Sin типа Mathf Unity для ее вычисления.
position.y = Mathf.Sin(position.x);
Что такое Mathf? read
Синусоидальная волна колеблется между -1 и 1. Она повторяется каждые 2π (pi) единицы, что составляет примерно 6,28. Поскольку X-координаты нашего графика находятся между -1 и 1, в настоящее время мы видим менее трети повторяющегося шаблона. Чтобы увидеть его целиком, масштабируем X на π, поэтому мы получаем f (x) = sin (πx). Мы можем использовать константу Mathf.PI в качестве приближения π.
position.y = Mathf.Sin(Mathf.PI * position.x);
Что такое синусоидальная волна и π? read
Чтобы оживить эту функцию, добавьте текущее время игры в X перед вычислением функции синуса. Если мы также масштабируем время на π, функция будет повторяться каждые две секунды. Поэтому используйте f (x, t) = sin (π (x + t)), где t - прошедшее время игры. Это ускорит синусоидальную волну с течением времени, сдвинув ее в направлении отрицательного X.
   position.y = Mathf.Sin(Mathf.PI * (position.x + Time.time));



Следующий урок - «Математические поверхности».

Перевод Беляев В.А. ака seaman

Комментариев нет:

Отправить комментарий