Разбор демо от Мэтта Старка

Разбор демо от Мэтта Старка

Твит, показывающий странный эффект, который я создал в Unity, стал довольно популярным, поэтому я решил подробнее рассказать, как этот эффект был создан.

Вот видео:

https://youtu.be/Z_RQenPprUc

Каждый дверной проем имеет скрытую стену, которая становится видимой, когда вы проходите триггер. Одновременно камерой генерируется и наносится на стену текстура. Вот как это выглядит под другим углом:

Эффект хитрого койота

Эффект можно поделить на две части: генерация текстуры и применение ее к объекту.

Генерация текстуры

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

Поле зрения камеры должно быть как можно меньше, но в то же время содержать весь объект. Если бы угол зрения был 60 градусов, но объект находился далеко, большая часть текстуры была бы потрачена впустую.

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

float maxAngle = 0;
Vector3 min = meshFilter.sharedMesh.bounds.min;
Vector3 max = meshFilter.sharedMesh.bounds.max;
// Перебрать каждый из 8 углов ограничивающей коробки
foreach (float bx in new float[] { min.x, max.x })
{
    foreach (float by in new float[] { min.y, max.y })
    {
        foreach (float bz in new float[] { min.z, max.z })
        {
            // Получить положение угла в пространстве камеры
            Vector3 cornerInCameraSpace = cam.transform.InverseTransformPoint(transform.TransformPoint(new Vector3(bx, by, bz)));
            // Найти горизонтальные и вертикальные углы между вектором нормали к камере и положением угла
            float horizontalAngle = Mathf.Abs(Mathf.Atan(cornerInCameraSpace.x / cornerInCameraSpace.z));
            float verticalAngle = Mathf.Abs(Mathf.Atan(cornerInCameraSpace.y / cornerInCameraSpace.z));
            // Если какой-то угол больше, чем хранящееся значение, заменить его
            maxAngle = Mathf.Max(maxAngle, horizontalAngle, verticalAngle);
        }
    }
}
// Установите поле зрения камеры на основе maxAngle. MaxAngle указывается в радианах, поэтому необходимо преобразовать в градусы. Maxangle также представляет только угол между нормалью и одним из углов к краю прямоугольника, но поле зрения - это угол между верхом и низом, поэтому его нужно умножить на два.
cam.fieldOfView = maxAngle * Mathf.Rad2Deg * 2;

Затем камера рендерит изображение, используя текстуру в качестве Render target’а. Я использовал текстуру 1024 х 1024 пикселей. Производимая текстура выглядит так:

Сгенерированная текстура

На следующем рисунке показана область текстуры, занимаемая ограничивающим прямоугольником объекта. Центр ограничивающего прямоугольника (пересечение красных линий) находится в центре текстуры (пересечение синих линий).

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

Диаграмма сгенерированной текстуры

Применение текстуры к объекту

Следующая задача — правильно отобразить текстуру на объекте. Если бы мы просто применили текстуру, используя UV-координаты объекта, она бы выглядела так (я включил каркас, чтобы сделать форму более понятной):

Неправильная проекция

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

Перед удалением временной камеры вычисляется матрица, которая преобразует мировое пространство в пространство экрана для камеры. Она находится путем умножения матрицы проекции на мировые координаты на матрицу координат камеры:

camMatrix = cam.projectionMatrix * cam.worldToCameraMatrix;

Эта матрица передается в шейдер вместе с текстурой. В вершинном шейдере вычисляется мировое положение вершины (worldPos — это float3, добавленный в структуру v2f).

o.worldPos = mul(unity_ObjectToWorld, v.vertex);

Во фрагментном шейдере положение в мировом пространстве проецируется в пространство экрана временной камеры и используется для выборки текстуры:

// Преобразование положения из мирового пространства в пространство камеры
float4 screenPos = mul(_WorldToCam, float4(i.worldPos.xyz, 1));
// Получение UV координат
float2 uv = screenPos.xy / screenPos.w;
uv = (uv + float2(1, 1)) / 2; // Convert it from the range -1 to 1 to the range 0 to 1
// Сэмплирование текстуры
fixed4 col = tex2D(_CamTex, uv);

Теперь текстура корректно отображается на объекте!

Правильная проекция

Альтернативным подходом было бы спроецировать текстуру только один раз и сохранить ее в другой текстуре. Это было бы быстрее, но труднее реализовать таким образом, чтобы обрабатывались объекты более сложные, чем плоскости.

Источник: https://matt.stark.scot/2019/11/06/wile-e-coyote-effect.html

Понравилась статья? Поделиться с друзьями:
Автор natalya
Переводит для Вас самые интересные статьи про разработку игр. По образованию физик-программист. Техническими переводами начала подрабатывать еще на старших курсах и постепенно это переросло в основное занятие. Интересуется гуманитарными технологиями, пробует себя в журналистике.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *