Cover

Ультра Эффекты | Часть 1: Нечто рыбное

Written on December 23rd, 2019

Fisheye линзы обрели популярность в 1900х за свою способность снимать с очень большими углами обзора использую сферическое отображение, вместо прямолинейной перспективы. Имя им дал американский физик Robert W. Wood, который хотел представить, как рыба видит мир под водой. Такие изображения характерны тем, что объекты в центре выглядят 'ближе', чем есть на самом деле. Сейчас мы используем такие линзы в самых различных случаях: от развлекательного медиа для передачи художественного замысла, до компьютерной графики, в которй с помощью таких 180-градусных виртуальных линз создаются карты окружений. Сегодня мы объединим творческий замысел и компьютерную графику и посмотрим на простой способ преввращения компьютерно-генерируемых изображений в Fisheye-стилизованные.

Автор фотографии: The Squirrels.

Виртуальные линзы


Обычные виртуальные камеры захватывают прямолинейные изображения - стены на них выглядят прямыми. Теоретически, вы можете использовать альтернативную проекционную матрицу для достижения эффекта "рыбьего глаза", но мы не будем изменять то, как камера отображает исходное изображение, - вместо этого мы создадим эффект постобработки, который имитирует эффект искажения, вносимый объективами "рыбий глаз", Чтобы сделать это, все, что нам нужно, это некоторе знание математики, чтобы немного поиграться с UV.

Для тех, кто не работал с UV раньше - эт очто-то вроде координатной системы, позволяющей нанести 2D текстуру (изображение) на 3D объект. В нашем случае "3D объектом" является экран, но мы его примем за двумерный, для простоты работы. UV координаты в данном случае будут применять двумерное изображение к двумерному объекту. Изображением у нас будт результат рендера сцены, прямо до того, как наш постэффект вступит в силу. Но перед тем, как посмотрим на работу такого шейдера, давайте поговорим о системе, которую я написал для данной серии уроков.

Please download the project repository from GitHub if you’d like to follow along!

Система шейдеров


В прошлой серии я предоставил два файла шейдеров для каждого: один полный, а другой с пробелами, которые вы можете заполнить с помощью этих уроков. Было несколько скриптов для облегчения разработки некоторых шейдеров, таких как "Размытие по Гауссу". На этот раз я упростил задачу - [репозиторий GitHub] (https://github.com/daniel-ilett/image-ultra) будет включать только полные версии шейдеров, и используемые скрипты будут менее запутанными.

Каждый шейдер имеет соответствующий скрипт, который включает в себя доступные в инспекторе переменные для каждой из специальных функций шейдера. Они используют ScriptableObject в качестве базового класса, поэтому вы можете создавать новые непосредственно в Unity - это позволит вам создавать несколько однотипных эффектов, но с разными параметрами. Считайте их пресетами! Создать их можно используя меню Create (Создать). Я подробно опишу, как эти ресурсы и сценарии создаются по мере продвижения в уроках.

Шейдер "Рыбий глаз"


Вообще, данные туториалы будут подразумевать, что у вас есть какое-то понимание синтаксиса шейдеров. Для начального ознакомления можно изучить эту серию уроков(прим.рел.: ссылка на оригинал), если не хочется вникать в основы, можно ограничиться этим(прим.рел.: тоже ссылка на оригинал). Наш шейдер использует много стандартного кода шейдера: структуры appdata иv2f и вершинный шейдер являются базовыми частями шейдера эффекта изображения в Unity. Мы собираемся добавить бочкообразное искажение в фрагментный шейдер, чтобы преобразовать исходное изображение в изображение типа "рыбий глаз" - оно названо так, потому что прямые линии имеют тенденцию изгибаться от центра, как это делают планки в бочке.

В шейдере мы добавим переменную _BarrelPower в Properties (Свойства) и включим её в код шейдера. Этот код доступен в Resources/Shaders/Fisheye.shader.

// In Properties.
_BarrelPower("Barrel Power", Float) = 1.0

// In shader.
uniform float _BarrelPower;

Затем мы определим функцию, которая отображает входные позиции в искаженные выходные позиции. Чтобы осмыслить, что делает функция, представьте, что она рисует линию от центра изображения до входной позиции, а затем расширяет эту линию с коэффициентом длины линии до степени _BarrelPower. Конечной точкой этой линии является новая позиция, которая затем корректируется в координату UV. Для этого мы будем вызывать функцию distort ().

float2 distort(float2 pos)
{
    float theta = atan2(pos.y, pos.x);
    float radius = length(pos);
    radius = pow(radius, _BarrelPower);
    pos.x = radius * cos(theta);
    pos.y = radius * sin(theta);

    return 0.5 * (pos + 1.0);
}

У нас есть отображение между входными позициями и выходными кординатами UV! Теперь все, что нам нужно сделать, это создать фрагментный шейдер. Фрагментный шейдер начинается с вычисления входных позиций.

fixed4 frag (v2f i) : SV_Target
{
    float2 xy = 2.0 * i.uv - 1.0;
    ...
}

Поскольку UV координаты рассчитываются так, что нижний левый угол изображения находится в (0, 0), а верхний правый в (1, 1), нам необходимо преобразовать их так, чтобы центр находился в точке (0, 0). и углы (1, 1), (-1, -1), (1, -1) и (-1, 1) соответственно. Теперь рассмотрим, что делать с пикселями за пределами формы "бочки". Функция distort будет работать с любыми пикселями внутри овала, касающегося краев экрана, но для пикселей за пределами этой области вывод получается грязным. Мы можем либо сохранить старые пиксели в этих областях путем выборки из исходной текстуры с оригинальной UV, либо мы можем отбросить их и "растянуть" эффект. Я выбрал последний вариант.

float d = length(xy);

if (d >= 1.0)
{
    discard;
}

float2 uv = distort(xy);
return tex2D(_MainTex, uv);

И это был весь код шейдера. Теперь нам нужно создать скрипт, дабы заставить его работать.

Scripting


Как я уже сказал, мы создадим скрипт, который наслежует другой скрипт, который я назвал BaseEffect. Этот скрипт наследует ScriptableObject, поэтому мы можем создавать его экземпляры на панели Project pane. Этот код доступен в Scripts/Image Effects/FisheyeEffect.cs.

using UnityEngine;

...
public class FisheyeEffect : BaseEffect
{
    ...
}

Чтобы создавать экземпляры скрипта из редактора, мы добавим директиву над определением класса.

[CreateAssetMenu(menuName = "Image Effects Ultra/Fisheye", order = 1)]

Она добавляет в Create menu (Меню создания) новую опцию, какую - скоро увидим. Далее мы добавим переменную, чтобы изменять значение показателя степени эффекта "бочки" - Я назвал её pow и она будет относиться к своёству шейдера _BarrelPower.

[SerializeField]
private float pow;

Чтобы использовать наш шейдер, мы должны создать материал, который его использует. BaseEffect предоставляет protected член с именем baseMaterial для этой цели, наряду с virtual методом OnCreate, который будет вызываться при запуске сцены. Мы будем использовать оба, и загрузим файл шейдера, используя Resources.Load вместо Shader.Find, чтобы эти шейдеры всегда были включены при сборке игры. Также внутри OnCreate мы отобразим привяжем pow к свойству _BarrelPower.

Прим. пер.: если вас смущают слова protected и virtual, то вам нужно глубже изучить C# (ещё там дальше по тексту будет слово abstract)

public override void OnCreate()
{
    baseMaterial = new Material(Resources.Load<Shader>("Shaders/Fisheye"));

    baseMaterial.SetFloat("_BarrelPower", pow);
}

Наконец, нам нужно применить эффект к каждому кадру. Для этого BaseEffect определяет abstract метод Render, который принимает начальное изображение и RenderTexture. По сути, он работает вместо OnRenderImage, но Unity не может вызвать эту функцию из ScriptableObject, т.к. это не наследник Component. Мы будем вызывать эту функцию вручную в другом месте.

public override void Render(RenderTexture src, RenderTexture dst)
{
    Graphics.Blit(src, dst, baseMaterial);
}

Это простейшая реализация функционала Render - простой вызов Graphics.Blit с двумя RenderTextures и baseMaterial.

Объединяя в целое


Теперь создадим новый ассет FisheyeEffect. Внутри в папке Effects, создаём с помощью Create>Image Effects Ultra>Fisheye эффект (можно вызвать и ПКМ-меню, а можно из тулбара). Новый ассет будет создан, а вы сможете менять в нём значение pow прямо в Inspector (Инспектор), как и в любом другом объекте в Unity. Теперь напишем короткий скрипт-наследник от MonoBehaviour, который поможет прицепить эффект к нашей камере и сложит пазл воедино.

using UnityEngine;

[RequireComponent(typeof(Camera))]
public class ImageEffect : MonoBehaviour
{
    [SerializeField]
    private BaseEffect effect;

    private void Awake()
    {
        effect.OnCreate();
    }

    private void OnRenderImage(RenderTexture src, RenderTexture dst)
    {
        effect.Render(src, dst);
    }
}

Этот скрипт - просто обёртка над нашим ассетом, который мы только что создали. Перенесите скрипт на камеру и вставьте в его effect поле наш эффект, затем запустите сцену. С pow равной 2, вы уидите эффект, похожий на тот, что на скриншоте снизу.

В заключение


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

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

От переводчика: следующий туториал будет переведён где-то к концу следующей недели, если смогу выкроить время. Если вам понравилась статья, то у автора оригинала есть Patreon, все ссылки есть на его сайте (а ещё можно задонатить мне, если вдруг вам захочется поддержать меня :3 )
UPD 01.01.20: мне ооооооочень лениво переводить