LoginSignup
6
4

More than 1 year has passed since last update.

ピクセルっぽさを残しつつうねうねを軽減する方法

Last updated at Posted at 2022-09-08

解像度の低いテクスチャを拡大して描画するとき
バイリニアを使うとボケが気になり
ポイントサンプリングを使うと整数倍以外ではうねうね

対策はPixelPerfectを導入するなどして整数倍に調整したりします

が 黒い領域ができたり 回転や拡大のアニメーションでうねることも

というわけで手軽になんとかならないかと
ボケを抑えつつうねうねを減らせるシェーダーを作りました

※個人の感想です

PixMov.gif
PixRot.gif
PixScl.gif

gifにしてみたけれどqiitaでは画像は1M以内でないとだめなんですね、
分割したりフレーム間引いたりでなんとか収めてみましたが雰囲気伝わるかな?

Linearは隣り合うピクセル同士の線形補間
Cosはコサイン補間でLinearよりは気持ちボケを減らせます
Edgeは名前は適当・・テクスチャのピクセルを跨ぐ所(辺の部分)だけCos補間しています
等倍近辺ではcosと変わらないですが、3倍以上になるとポイントに近くなります
Pointは最近傍サンプリング

というわけでコードはこちらに

Pixelish.shadr
Shader "Pixelish"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color("Color", Color) = (1, 1, 1, 0.5)
        [KeywordEnum(Linear, Cos, Edge, Point)]_Filter("Filter", Int) = 0
    }
    SubShader
    {
        Tags {"Queue" = "Transparent" "RenderType" = "Transparent"}
        LOD 100

        Pass
        {
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _FILTER_LINEAR _FILTER_COS _FILTER_EDGE _FILTER_POINT

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                UNITY_VERTEX_OUTPUT_STEREO
            };
            Texture2D _MainTex;
            SamplerState xx_Linear_Clamp_Sampler;
            float4 _MainTex_ST;
            float4 _MainTex_TexelSize;
            float4 _Color;

            half4 SampleMainTex(float2 uv)
            {
#if _FILTER_LINEAR
                uv = uv;
#endif
#if _FILTER_COS
                float2 ux = uv * _MainTex_TexelSize.zw + 0.5;
                float2 ui = floor(ux);
                float2 uf = ux - ui;
                uv = ui * _MainTex_TexelSize.xy - 0.5 * cos(uf * 3.141592) * _MainTex_TexelSize.xy;
#endif
#if _FILTER_EDGE
                float2 ux = uv * _MainTex_TexelSize.zw + 0.5;
                float2 ui = floor(ux);
                float2 uf = ux - ui;
#if 1
                float2 ddxuv = ddx(ux);
                float2 ddyuv = ddy(ux);
                float2 ll = sqrt(ddxuv * ddxuv + ddyuv * ddyuv);
                float2 scale = 1.0 / saturate(ll);
                uf = saturate(0.5 + (uf - 0.5) * scale);
#endif
                uv = ui * _MainTex_TexelSize.xy - 0.5 * cos(uf * 3.141592) * _MainTex_TexelSize.xy;
                //uv = ui * _MainTex_TexelSize.xy + uf * _MainTex_TexelSize.xy;
#endif
#if _FILTER_POINT
                float2 ux = uv * _MainTex_TexelSize.zw;
                float2 ui = floor(ux);
                uv = ui * _MainTex_TexelSize.xy + 0.5 * _MainTex_TexelSize.xy;
#endif
                half4 col = _MainTex.SampleLevel(xx_Linear_Clamp_Sampler, uv, 0);
                return col;
            }

            v2f vert(appdata v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            half4 frag(v2f i) : SV_Target
            {
                half4 col = SampleMainTex(i.uv);
                return col * _Color;
            }
            ENDCG
        }
    }
}

使い方としては低解像度のRenderTargetにピクセルパーフェクトで描画後
このシェーダーで端末の解像度に拡大描画するか
最初から高解像度のターゲットにこのシェーダーでオブジェクトを描画するか
お好みで

テクスチャの設定によらないようインラインサンプラーを使っています
テクスチャをバイリニアの設定にしてあるならばtex2Dlodに置き換えられます

またmipmapを無効にするためSampleLevelを使っています

Edge版はスケールが固定ならddxなど使わないでプロパティから与えるのもあり
vertexシェーダーで計算しておくのでもいいかも

PointなのにLinearサンプラーを使っているのはテストと比較のためなので
実用する場合は書き換えてください

縮小については下手に何かするとピクセル感が無くなるので
ジャリジャリするけれどそのままにしています

というわけでこんな感じですがいかがでしょうか?
こういうのハードで実装してくれたほうが低コストでいいんですけどねー

補足

プラットフォームによってはボケがきつい場合があるかも?
テストはWindows11+Ryzen6800の環境で行っていて
UnityEditor上でOpenGLとDirectX11で見ています

気になるのはテクスチャのサンプリング位置。
バイリニアの時に小数点が0のときそのピクセルが100%になることを期待していたら
どうやら0は中間点の50%になる模様テクセルの範囲は-0.5~+0.5みたい。
なのでコードではその分オフセットしています。
以前はUnityでもUNITY_HALF_TEXEL_OFFSETで判定できたのだけれど
現在はどこかで吸収されているらしく定義されなくなっている?
おそらくマトリクスでやってると思うので
今回のようにフラグメントで直接いじる場合は環境によっては問題が出るかもしれません
対策としてはプラットフォームを直接確認するかポイントサンプルで自力でLerpするかですね。

もしかしたらどこかでうまいことやってて気にしなくてもいいのかもしれないけれど。

ちなみになぜピクセル半分のオフセットがあるかというと
おそらくポイントサンプルとバイリニアと切り替えた時のずれを出さないため?

GPUの初期のころはテクセルセンターの問題やラスタライザのポリシーというか
実装の差とかでピクセルの制御がなかなか難しかったけれど
現在は大体どのGPUも似た感じに収斂してきたのと解像度が上がったのもあり
少々のピクセルのずれは気にされなくなってしまいましたね

謝辞

サンプルの画像 ©2014 CloverLab.,Inc.
クローバーラボ株式会社さんから提供されている「ゆぐドラシル」素材を使わせていただきました
ありがとうございます!
ゆぐドラシル

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4