Расширяем шейдер PlayStation 1 — Аффинная текстурная интерполяция в Unity

В прошлой статье мы реализовали эффект дрожания вершин, характерный для PlayStation 1, воссоздав ограниченную точность вычислений, свойственную старым консолям. Теперь добавим еще одну ключевую особенность рендеринга PS1 — аффинную интерполяцию текстур.

На оригинальной PlayStation не использовалась перспективная коррекция текстур, что приводило к заметным искажениям при изменении угла наклона полигонов. Этот эффект был побочным результатом работы GPU консоли и активно использовался в таких играх, как Metal Gear Solid и Tomb Raider. Давайте воссоздадим этот визуальный артефакт в Unity!

Как работала аффинная интерполяция текстур на PS1?

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

Пример эффекта:

Современная перспективная интерполяция:



Аффинная интерполяция (PS1-стиль):

На PS1 это приводило к «плавающим» текстурам, особенно заметным на больших плоскостях.

Реализация эффекта в Unity

Воссоздать этот эффект в Unity можно путем отключения перспективной коррекции текстур. Для этого нам нужно изменить способ вычисления UV-координат в пиксельном шейдере.

Шаг 1: Создание шейдера

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

Shader "Custom/PS1AffineTexture"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            sampler2D _MainTex;

            struct appdata_t
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata_t v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;  // Передаем UV без деления на w
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }
    }
}

В отличие от стандартных шейдеров, мы не делим UV на w, что создает эффект аффинной интерполяции.

Шаг 1: Дополним наш основной шейдер новой техникой

Так же введем возможность отключать аффинные преобразования в настройках материала

Shader "Custom/PS1Shader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _JitterStrength ("Jitter Strength", Range(0, 1)) = 0.02
        _UseAffine ("Use Affine Mapping", Float) = 1
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            sampler2D _MainTex;
            float _JitterStrength;
            float _UseAffine;

            struct appdata_t
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            float2 Quantize(float2 pos, float step)
            {
                return floor(pos / step + 0.5) * step;
            }

            v2f vert (appdata_t v)
            {
                v2f o;
                
                // Дрожание вершин
                float jitter = _JitterStrength * (sin(v.vertex.x * 10.0) + cos(v.vertex.y * 10.0));
                v.vertex.xy += jitter;

                // Преобразование в Clip Space с квантованием координат
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.vertex.xy = Quantize(o.vertex.xy, _JitterStrength);

                // Включение/выключение аффинной интерполяции
                if (_UseAffine > 0.5)
                    o.uv = v.uv;  // Без перспективной коррекции (аффинная интерполяция)
                else
                    o.uv = v.uv / o.vertex.w;  // Перспективная коррекция

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }
    }
}

Добавлен параметр _UseAffine, который работает как чекбокс в Unity:

  • 1 (по умолчанию) — аффинная интерполяция ВКЛЮЧЕНА.
  • 0 — аффинная интерполяция ВЫКЛЮЧЕНА, используется стандартная перспективная коррекция.

Теперь можно легко включать и выключать эффект PS1 без необходимости менять шейдер!

Результаты и визуальные особенности

После применения этого шейдера текстуры начнут «плыть» при изменении угла наклона поверхности, создавая узнаваемый эффект PS1.

Плюсы:

  • Полное погружение в атмосферу ретро-графики.
  • Идеально для стилизации под старые консоли.

Минусы:

  • Эффект может выглядеть странно на высокополигональных моделях.
  • Не подходит для реалистичных сцен.

Заключение

Теперь у нас есть два эффекта, воссоздающих стиль PlayStation 1 в Unity: дрожание вершин и аффинная интерполяция текстур. Эти особенности, присущие старым играм, помогут придать вашей игре винтажный вид и уникальный стиль