【Unity】【Shader】異方性フィルタリングを実装する

異方性フィルタリングの具体的な実装例が
見つからなかったので自分で考えてみました

ちなみに異方性フィルタリングは hlsl や glsl に組み込まれているので
自分で実装する必要は無いです

第1段階 単純なアンチエイリアス

まず異方性フィルタリングがない状態の
テクスチャサンプリングを再現します

AntiAliasing_1.shader の内容

// MIT License
// Copyright (c) 2022 huwahuwa2017
// https://github.com/huwahuwa2017/huwahuwa-memo/blob/main/LICENSE

Shader "Custom/AntiAliasing_1"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "black" {}

        [Toggle(_)]
        _Difference("Difference", Int) = 0
        
        [Toggle(_)]
        _ViewOriginalColor("ViewOriginalColor", Int) = 0

        _OffsetLOD("OffsetLOD", Range(-4.0, 4.0)) = 0.0
    }

    SubShader
    {
        Pass
        {
            Cull Off

            CGPROGRAM

            #pragma vertex VertexShaderStage
            #pragma fragment FragmentShaderStage

            #include "AntiAliasing_1.hlsl"

            ENDCG
        }
    }
}

AntiAliasing_1.hlsl の内容

// MIT License
// Copyright (c) 2022 huwahuwa2017
// https://github.com/huwahuwa2017/huwahuwa-memo/blob/main/LICENSE

#include "UnityCG.cginc"

struct I2V
{
    float4 lPos : POSITION;
    float2 uv : TEXCOORD0;
};

struct V2F
{
    float4 cPos : SV_POSITION;
    float2 uv : TEXCOORD0;
};

sampler2D _MainTex;
float4 _MainTex_TexelSize;

uint _Difference;
uint _ViewOriginalColor;
float _OffsetLOD;

V2F VertexShaderStage(I2V input)
{
    V2F output = (V2F) 0;
    output.cPos = UnityObjectToClipPos(input.lPos);
    output.uv = input.uv;
    return output;
}

half4 FragmentShaderStage(V2F input) : SV_Target
{
    float2 size = _MainTex_TexelSize.zw;
    
    float2 uv = input.uv;
    uv = mul(float2x2(0.707, 0.707, -0.707, 0.707), uv);
    
    // uv の値が大きくなると誤差も大きくなる
    // かといって uv を frac 関数に直接入れると、
    // 繰り返し部分の ddx, ddy が正しく計算できなりエイリアスが発生する
    // なので少し工夫する必要がある
    uv.x += frac(_Time.x);
    
    
    
    float2 dx, dy;
    {
        float2 temp00 = uv * size;
        dx = ddx(temp00);
        dy = ddy(temp00);
    }
    
    float lod = max(length(dx), length(dy));
    lod = log2(lod) + _OffsetLOD;
    
    half4 resultColors = tex2Dlod(_MainTex, float4(uv, 0.0, lod));
    
    
    
    // 比較用
    half4 originalColor = tex2D(_MainTex, uv);
    
    half4 output = _ViewOriginalColor ? originalColor : resultColors;
    output = _Difference ? abs(resultColors - originalColor) : output;
    return output;
}

この Shader の結果は、 Unity で異方性フィルタリングをオフ
(Project Settings の Quality の Anisotropic Textures を Disabled)
に設定した状態で tex2D() を使用した時とほぼ同じ結果になります

第1段階 問題点と修正方法

この Shader ではエイリアスを防ぐことはできるのですが、
テクスチャが張り付けられたポリゴンを急な角度で見ると、
遠くの色がぼやけすぎています


なぜぼやけすぎてしまうのでしょうか?

この図は急な角度かつ遠くのテクスチャを見たときの、
画面上の1ピクセルの範囲を uv 座標上に投影したイメージ図です

それぞれの辺は変数 dx, dy に対応しています

この赤色の長方形の範囲の、色の平均を計算できれば理想的なサンプリングになります
しかし、
float lod = max(length(dx), length(dy));
の計算式では、次の図の緑色の範囲を平均化した色になってしまいます
平均化する範囲が赤色の領域と比べて大きすぎるので、ぼけすぎてしまうのです

かといって
float lod = min(length(dx), length(dy));
に変更すると、
平均化する範囲が赤色の領域と比べて小さすぎるので、ジャギが発生しやすくなります

これを解決する方法があります
長方形を小さな正方形に分割してしまう方法です


第2段階 単純な異方性フィルタリング

長方形を小さな正方形に分割する仕組みを、
Shader で実装するとこのようになります

AntiAliasing_2.shader の内容

// MIT License
// Copyright (c) 2022 huwahuwa2017
// https://github.com/huwahuwa2017/huwahuwa-memo/blob/main/LICENSE

Shader "Custom/AntiAliasing_2"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "black" {}

        [Toggle(_)]
        _Difference("Difference", Int) = 0
        
        [Toggle(_)]
        _ViewOriginalColor("ViewOriginalColor", Int) = 0

        _OffsetLOD("OffsetLOD", Range(-4.0, 4.0)) = 0.0

        [IntRange]
        _MaxAnisotropy("MaxAnisotropy", Range(1, 16)) = 16
    }

    SubShader
    {
        Pass
        {
            Cull Off

            CGPROGRAM

            #pragma vertex VertexShaderStage
            #pragma fragment FragmentShaderStage

            #include "AntiAliasing_2.hlsl"

            ENDCG
        }
    }
}

AntiAliasing_2.hlsl の内容

// MIT License
// Copyright (c) 2022 huwahuwa2017
// https://github.com/huwahuwa2017/huwahuwa-memo/blob/main/LICENSE

#include "UnityCG.cginc"

struct I2V
{
    float4 lPos : POSITION;
    float2 uv : TEXCOORD0;
};

struct V2F
{
    float4 cPos : SV_POSITION;
    float2 uv : TEXCOORD0;
};

sampler2D _MainTex;
float4 _MainTex_TexelSize;

uint _Difference;
uint _ViewOriginalColor;
float _OffsetLOD;

uint _MaxAnisotropy;

V2F VertexShaderStage(I2V input)
{
    V2F output = (V2F) 0;
    output.cPos = UnityObjectToClipPos(input.lPos);
    output.uv = input.uv;
    return output;
}

half4 FragmentShaderStage(V2F input) : SV_Target
{
    float2 size = _MainTex_TexelSize.zw;
    
    float2 uv = input.uv;
    uv = mul(float2x2(0.707, 0.707, -0.707, 0.707), uv);
    
    // uv の値が大きくなると誤差も大きくなる
    // かといって uv を frac 関数に直接入れると、
    // 繰り返し部分の ddx, ddy が正しく計算できなりエイリアスが発生する
    // なので少し工夫する必要がある
    uv.x += frac(_Time.x);
    
    
    
    float2 dx, dy;
    {
        float2 temp00 = uv * size;
        dx = ddx(temp00);
        dy = ddy(temp00);
    }
    
    // 長方形の長辺と短辺
    float longSideLength, shortSideLength;
    float2 longSideVector;
    {
        float temp30 = length(dx);
        float temp31 = length(dy);
        longSideLength = max(temp30, temp31);
        shortSideLength = min(temp30, temp31);
        
        longSideVector = (longSideLength == temp30) ? dx : dy;
    }
    
    float2 samplingDir = longSideVector / size;
    
    uint samplingCount = ceil(longSideLength / shortSideLength);
    samplingCount = clamp(samplingCount, 1, _MaxAnisotropy);
    
    float2 samplingMove = samplingDir / samplingCount;
    
    float2 samplingPos = uv - samplingDir * 0.5;
    samplingPos = samplingPos + samplingMove * 0.5;
    
    float lod = max(shortSideLength, longSideLength / samplingCount);
    lod = log2(lod) + _OffsetLOD;
    
    float4 resultColors = 0.0;
    
    for (uint count = 0; count < samplingCount; ++count)
    {
        resultColors += tex2Dlod(_MainTex, float4(samplingPos, 0.0, lod));
        samplingPos += samplingMove;
    }

    resultColors = resultColors / samplingCount;
    
    
    
    // 比較用
    half4 originalColor = tex2D(_MainTex, uv);
    
    half4 output = _ViewOriginalColor ? originalColor : resultColors;
    output = _Difference ? abs(resultColors - originalColor) : output;
    return output;
}



第2段階 問題点と修正方法

うまくいっているように見えますが、まだ問題があります
特定の方向に傾けると、再び遠くの色がぼやけすぎるようになります


このとき、画面上の1ピクセルの範囲を uv 座標上に投影したイメージ図はこのようになります

辺の長さがほぼ同じです
つまり length(dx) と length(dy) が近い値になり、次の図の緑色の範囲を平均化した色になってしまいます

これを解決する方法があります

まず赤色の範囲を構成する4辺の中点を通過する楕円を生成します

その楕円の長軸ベクトルと短軸ベクトルから平均化する範囲を計算します

そのあとは 「第2段階 単純な異方性フィルタリング」 と同じです

第3段階 異方性フィルタリング

まず赤色の範囲を構成する4辺の中点を通過する楕円の式を用意します
変数 dx と dy から行列を作って、その逆行列と円の方程式を組み合わせるだけです

楕円の長軸ベクトルと短軸ベクトルを計算するためには固有ベクトルを求める必要があり、
固有ベクトルを求めるためには式を二次形式の形にする必要があるので式変形します

行列の各成分の値の計算式を求めます

行列が出来上がったので、Shaderで実装します

AnisotropicFiltering.shader の内容

// MIT License
// Copyright (c) 2022 huwahuwa2017
// https://github.com/huwahuwa2017/huwahuwa-memo/blob/main/LICENSE

Shader "Custom/AnisotropicFiltering"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "black" {}

        [Toggle(_)]
        _Difference("Difference", Int) = 0
        
        [Toggle(_)]
        _ViewOriginalColor("ViewOriginalColor", Int) = 0

        _OffsetLOD("OffsetLOD", Range(-4.0, 4.0)) = 0.0

        [IntRange]
        _MaxAnisotropy("MaxAnisotropy", Range(1, 16)) = 16
    }

    SubShader
    {
        Pass
        {
            Cull Off

            CGPROGRAM

            #pragma vertex VertexShaderStage
            #pragma fragment FragmentShaderStage

            #include "AnisotropicFiltering.hlsl"

            ENDCG
        }
    }
}

AnisotropicFiltering.hlsl の内容

// MIT License
// Copyright (c) 2022 huwahuwa2017
// https://github.com/huwahuwa2017/huwahuwa-memo/blob/main/LICENSE

#include "UnityCG.cginc"

struct I2V
{
    float4 lPos : POSITION;
    float2 uv : TEXCOORD0;
};

struct V2F
{
    float4 cPos : SV_POSITION;
    float2 uv : TEXCOORD0;
};

sampler2D _MainTex;
float4 _MainTex_TexelSize;

uint _Difference;
uint _ViewOriginalColor;
float _OffsetLOD;

uint _MaxAnisotropy;

V2F VertexShaderStage(I2V input)
{
    V2F output = (V2F) 0;
    output.cPos = UnityObjectToClipPos(input.lPos);
    output.uv = input.uv;
    return output;
}

half4 FragmentShaderStage(V2F input) : SV_Target
{
    float2 size = _MainTex_TexelSize.zw;
    
    float2 uv = input.uv;
    uv = mul(float2x2(0.707, 0.707, -0.707, 0.707), uv);
    
    // uv の値が大きくなると誤差も大きくなる
    // かといって uv を frac 関数に直接入れると、
    // 繰り返し部分の ddx, ddy が正しく計算できなりエイリアスが発生する
    // なので少し工夫する必要がある
    uv.x += frac(_Time.x); 
    
    
    
    float2 dx, dy;
    {
        float2 temp00 = uv * size;
        dx = ddx(temp00);
        dy = ddy(temp00);
    }
    
    // ┌          ┐
    // │dx.x  dy.x│
    // │dx.y  dy.y│ = M
    // └          ┘
    
    //             ┌            ┐
    //      T  -1  │m00    m0110│
    // ( M M  )   =│m0110  m11  │ を計算する
    //             └            ┘
    float m00, m0110, m11;
    {
        float invDet = dx.x * dy.y - dy.x * dx.y;
        invDet = 1.0 / (invDet * invDet);
        
        m00 = (dx.y * dx.y + dy.y * dy.y) * invDet;
        m0110 = -(dx.x * dx.y + dy.x * dy.y) * invDet;
        m11 = (dx.x * dx.x + dy.x * dy.x) * invDet;
    }
    
    //      T  -1
    // ( M M  )   の固有値を計算する
    float eigen0, eigen1;
    {
        float temp10 = m00 + m11;
        float temp11 = m00 * m11 - m0110 * m0110;
        float temp12 = sqrt(temp10 * temp10 - 4.0 * temp11);

        eigen0 = (temp10 + temp12) * 0.5;
        eigen1 = (temp10 - temp12) * 0.5;
    }
    
    //      T  -1
    // ( M M  )   の固有ベクトルを計算して正規化する
    float2 eigenVector;
    {
        float temp20 = eigen1 - m00;
        float temp21 = eigen1 - m11;
        
        bool flag0 = abs(temp20) > abs(temp21);
        float2 temp22 = flag0 ? float2(m0110, temp20) : float2(temp21, m0110);
        
        float temp23 = sqrt(dot(temp22, temp22));
        eigenVector = (temp23 > 0.0) ? temp22 / temp23 : 0.0;
    }
    
    // 楕円の長半径と短半径
    float longRadius, shortRadius;
    {
        longRadius = 1.0 / sqrt(eigen1);
        shortRadius = 1.0 / sqrt(eigen0);
        
        float temp30 = length(dx);
        float temp31 = length(dy);
        longRadius = isnan(longRadius) ? max(temp30, temp31) : longRadius;
        shortRadius = isnan(shortRadius) ? min(temp30, temp31) : shortRadius;
    }
    
    float2 samplingDir = eigenVector * longRadius / size;
    
    uint samplingCount = ceil(longRadius / shortRadius);
    samplingCount = clamp(samplingCount, 1, _MaxAnisotropy);
    
    float2 samplingMove = samplingDir / samplingCount;
    
    float2 samplingPos = uv - samplingDir * 0.5;
    samplingPos = samplingPos + samplingMove * 0.5;
    
    float lod = max(shortRadius, longRadius / samplingCount);
    lod = log2(lod) + _OffsetLOD;
    
    float4 resultColors = 0.0;
    
    for (uint count = 0; count < samplingCount; ++count)
    {
        resultColors += tex2Dlod(_MainTex, float4(samplingPos, 0.0, lod));
        samplingPos += samplingMove;
    }

    resultColors = resultColors / samplingCount;
    
    
    
    // 比較用
    half4 originalColor = tex2D(_MainTex, uv);
    
    half4 output = _ViewOriginalColor ? originalColor : resultColors;
    output = _Difference ? abs(resultColors - originalColor) : output;
    return output;
}