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