Создание шейдера с эффектом дрожания вершин для Unity в стиле PlayStation 1
В предыдущей статье мы реализовали довольно неплохой эффект старого рендера из ранних 90х, однако, ощущения от него будут неполными, если мы не введем другие характерные для того времени ограничения. Нет-нет, 10 фпс и имитацию слабых компьютеров вводить не будем, но постараемся ввести один значимый для ретро-рендера эффект. Возможно он не являются частью общей парадигмы, особенно в области рендеринга на РС, но добавить особую атмосферу. Речь идет, конечно же, об эффекте дрожания вершин, характерный для Playstation 1.
Эффект «дрожания вершин» в PlayStation 1, был следствием ограниченной точности расчетов, использовавшихся на этом железе. Поскольку PlayStation 1 использовала фиксированную точку для вычисления координат вершин, это приводило к визуальным артефактам, когда позиции вершин «дрожали» при перемещении камеры или объектов. Мы, как и в прошлый раз, попробуем воссоздать данный эффект только с помощью шейдера в Unity, без использования дополнительных скриптов на C#.
Основная идея простая — нужно будет обойти все доступные шейдеру вершины и округлить их позиции относительно проекционных координат .
Основы шейдера
Напишем заготовку для шейдера, в которой укажем будущие параметры для материалы и объявим структуры и переменные
Shader "rikovmike/PS1VertexJitter"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_JitterAmount ("Jitter Amount", Range (0.0, 0.1)) = 0.05
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
};
sampler2D _MainTex;
float _JitterAmount;
ENDCG
}
}
FallBack "Diffuse"
}Здесь мы задаём два параметра для будущего материала: _MainTex — текстура и _JitterAmount — слайдер для точной настройки силы эффекта дрожания.
Еще нам, конечно же, понадобятся две структуры:
appdata— данные вершин, передаваемые в вершинный шейдер из Unityv2f— данные, передаваемые из вершинного шейдера в пиксельный шейдер и, в конечном итоге, в графический конвейер
Добавим функцию округления позиций вершин. Мы применяем округление только к clipPos.xy, то есть к координатам, которые уже находятся в проекционных координатах (после всех трансформаций объекта). Это даст эффект «дрожания», как на PlayStation 1, когда камера или объект движется. Параметр clipPos будет передаваться в функцию, как и параметр jitterAmount, который позволяет контролировать, насколько сильным будет эффект дрожания.
// Функция для округления координат вершин в clip space
float4 JitterVertex(float4 clipPos, float jitterAmount)
{
// Применяем квантование только на позиции в clip space (экранные координаты)
clipPos.xy = round(clipPos.xy / jitterAmount) * jitterAmount;
return clipPos;
}Округление в clip space означает, что каждая вершина будет «привязана» к определенным фиксированным позициям на экране. При движении камеры или объекта из-за этого квантования вершины будут перескакивать с одного положения на другое, создавая желаемый эффект «дрожания».
Начинаем колдовать
Пишем «вершинный» блок шейдера.
v2f vert(appdata v)
{
v2f o;
float4 clipPos = UnityObjectToClipPos(v.vertex);
// Вызываем функцию для "дрожания" вершин
o.pos = JitterVertex(clipPos, _JitterAmount);
o.uv = v.uv;
return o;
}Здесь мы сначала получаем clipPos функцией UnityObjectToClipPos для нашей вершины. Затем получаем новую позицию вершины через вызов написанной ранее функции JitterVertex, которой передаем полученный clipPos и наш параметр _JitterAmount из инспектора.
Возвращаем новые данные вершины. UV-координаты оставляем без изменений.
Параметр _JitterAmount нужно держать в пределах от 0.01 до 0.1, этого достаточно для заметного эффекта. Однако, никто не запрещает эксперименты — все ваших руках.
Осталось только добавить фрагментную (пиксельную) часть, в которой мы просто отдаем текстуру, как есть.
fixed4 frag(v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}Полный код шейдера
Shader "rikovmike/PS1VertexJitter"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_JitterAmount ("Jitter Amount", Range (0.0, 0.1)) = 0.05
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
};
sampler2D _MainTex;
float _JitterAmount;
// Функция для округления координат до целого
float4 JitterVertex(float4 clipPos, float jitterAmount)
{
// Применяем квантование только на позиции в clip space (экранные координаты)
clipPos.xy = round(clipPos.xy / jitterAmount) * jitterAmount;
return clipPos;
}
v2f vert(appdata v)
{
v2f o;
float4 clipPos = UnityObjectToClipPos(v.vertex);
// Вызываем функцию для "дрожания" вершин
o.pos = JitterVertex(clipPos, _JitterAmount);
o.uv = v.uv;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
FallBack "Diffuse"
}Настройка текстуры
Шейдер использует стандартную текстуру, которую можно задать в Unity через инспектор. Текстура рендерится через стандартную функцию tex2D, которая отображает текстуру по UV-координатам, переданным из вершинного шейдера. С помощью бегунка _JitterAmount плавно регулируем эффект дрожания. В зависимости от проекта, свойства самой текстуры настраиваются индивидуально, но стилистически, конечно же, желательно выключить фильтрацию и использовать текстуры с небольшим разрешением, вроде 64х64 или 128х128.

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