0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】四角形の変形によるUVの褶曲をシェーダーで補正する

Posted at

概要

メッシュを変形するとUVも一緒に曲がりますが,UVは三角形で補間されるので,ポリゴンフローで見るとガビガビになってしまいます.
これをシェーダーで補正します.ただし,本手法は制約が厳しいので応用先は限定的です.

image.png

前提条件

本手法でUVを補正するためには,メッシュが以下の条件を満たしている必要があります.

  • メッシュがすべて四角形で構成されている
  • UVがすべて格子状に配置されている
    • Blenderでカーブからメッシュを生成したときにできるUVなど
    • 格子線はU軸・V軸に平行でさえあれば,その間隔は一定でなくてもよい
  • 四角形を構成する4頂点の座標とUV座標をジオメトリシェーダー内で取得できる
  • 四角形を三角形化する向きがメッシュ全体で同じ

上記の通り制約が厳しく,メッシュそのものをジオメトリシェーダーで生成するようなシェーダーでないと適用が難しいかと思います.

補正方法

UV座標を,その座標における四角形と三角形の辺の長さの比率を新たにUVとして定義し,元のUVを新たなUVで補間することによって補正します.
下の図でいうと,( 赤 ÷ 青 ) を四角形内のローカルV座標として定義し,四角形の下辺・上辺のグローバルV座標 (本来のUV) をローカルV座標で補間します.U座標も同様です.

image.png

図を見てわかる通り,補正したいUV座標がどちらの三角形に含まれるかで場合分けが必要になります.
この方法は正確な補正ではないですが,見た目の改善には十分です.

実装例

上の図と同じ向きに三角形を作る前提で実装します.

UvSmoothing.shader
Shader "Test/UvSmoothing"
{
    Properties
    {
        [Toggle] _EnableSmoothing("Enable Smoothing", Float) = 1
    }
    SubShader
    {
        Pass
        {
            Tags { "RenderType"="Opaque" "Queue"="Geometry" }
    
            CGPROGRAM
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag
			#pragma multi_compile_instancing

            #pragma target 4.6
            
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
				UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2g
            {
                float4 vertex : POSITION;
				UNITY_VERTEX_INPUT_INSTANCE_ID
				UNITY_VERTEX_OUTPUT_STEREO
            };

            struct g2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 uvRange : TEXCOORD1;
                float2 uvData : TEXCOORD2;
                float4 lenData : TEXCOORD3;
				UNITY_VERTEX_OUTPUT_STEREO
            };

            float _EnableSmoothing;

            v2g vert (appdata v)
            {
                v2g o;
				UNITY_SETUP_INSTANCE_ID(v);
				UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
				UNITY_TRANSFER_INSTANCE_ID(v, o);
                o.vertex = v.vertex;
                return o;
            }

            [maxvertexcount(6)]
            void geom (triangle v2g IN[3], uint primitive_index : SV_PrimitiveID, inout TriangleStream<g2f> stream)
            {
                if (primitive_index != 0)
                {
                    return;
                }
            
                UNITY_SETUP_INSTANCE_ID(IN[0]);
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(IN[0]);

                // 四角形の4つの頂点(適当に配置)
                float4 p00 = float4(0, 0, 0, 1);
                float4 p01 = float4(0, 1, 0, 1);
                float4 p10 = float4(1, 0.1, 0, 1);
                float4 p11 = float4(1.1, 0.7, 0, 1);

                // 元のUV
                float2 uv00 = float2(0, 0);
                float2 uv01 = float2(0, 1);
                float2 uv10 = float2(1, 0);
                float2 uv11 = float2(1, 1);

                float4 lenData = float4(
                    length(p10.xyz - p00.xyz),  // 下辺
                    length(p11.xyz - p01.xyz),  // 上辺
                    length(p01.xyz - p00.xyz),  // 左辺
                    length(p11.xyz - p10.xyz)   // 右辺
                );

                g2f o00, o01, o10, o11;
                UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(IN[0], o00);
                UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(IN[0], o01);
                UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(IN[0], o10);
                UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(IN[0], o11);

                o00.vertex = UnityObjectToClipPos(p00);
                o01.vertex = UnityObjectToClipPos(p01);
                o10.vertex = UnityObjectToClipPos(p10);
                o11.vertex = UnityObjectToClipPos(p11);

                o00.uv = uv00;
                o01.uv = uv01;
                o10.uv = uv10;
                o11.uv = uv11;

                // 左下のUV座標と右上のUV座標
                o00.uvRange = float4(uv00, uv11);
                o01.uvRange = float4(uv00, uv11);
                o10.uvRange = float4(uv00, uv11);
                o11.uvRange = float4(uv00, uv11);

                // 四角形1つ分を[0,1]に正規化したUV座標
                o00.uvData = float2(0, 0);
                o01.uvData = float2(0, 1);
                o10.uvData = float2(1, 0);
                o11.uvData = float2(1, 1);

                o00.lenData = lenData;
                o01.lenData = lenData;
                o10.lenData = lenData;
                o11.lenData = lenData;

                // 右下と左上に分割するように三角形を作る
                stream.Append(o00);
                stream.Append(o01);
                stream.Append(o11);
                stream.RestartStrip();
                stream.Append(o00);
                stream.Append(o11);
                stream.Append(o10);
                stream.RestartStrip();
            }

            float4 frag (g2f i) : SV_Target
            {
                float2 uv;

                if (_EnableSmoothing < 0.5)
                {
                    // 普通のUV
                    uv = i.uv;
                }
                else
                {
                    // UVの補正
                    uv = i.uvData;  // quad内のローカルuv
                    float2 la = i.lenData.xz;  // 下辺と左辺の長さ
                    float2 lb = i.lenData.yw;  // 上辺と右辺の長さ
                    float2 l = lerp(la, lb, uv.yx);
                    if (uv.x > uv.y)
                    {
                        // 右下の三角形
                        uv = float2(1 - la.x / l.x * (1 - uv.x), lb.y / l.y * uv.y);
                    }
                    else
                    {
                        // 左上の三角形
                        uv = float2(lb.x / l.x * uv.x, 1 - la.y / l.y * (1 - uv.y));
                    }
                    uv = lerp(i.uvRange.xy, i.uvRange.zw, saturate(uv));
                }

                float4 col = float4(0, 0, 0, 1);
                col.xy = frac(uv * 10);
                return col;
            }
            ENDCG
        }
    }
}

適当なメッシュに適用すれば,冒頭の画像の通りの結果が得られます.

雑記

「四角形の4頂点を取得する」というのがなかなかに難しいので,Unity の通常の SkinnedMesh に適用するのは難しいと思います.triangleadj が使えれば何とかなりそうなんですが,Unity では使えないらしいです.
筆者の用途では,GPUパーティクル的にデータ用テクスチャから点の位置を取得してシェーダー内でメッシュを組み立てるため,この手法がギリギリ使えます.
描画負荷を減らすCGの歴史に逆行している気もしますが... まあいいでしょう.

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?