uGUI…というか3Dで2Dの描画するときの鬼門の一つに台形があります。
表示してみればわかりますがテクスチャが変になります。
この記事では上底と下底が並行な台形(平行四辺形含む)に絞った描画方法を模索しています。
そうでない頂点が自由な四角形はこの記事の方法ではキレイに描画できません。
とりあえずuGUIで台形描画してみる
まず何も考えずに台形描画してみます。
これが
こうなります。
明らかにUVがおかしいです。
UVがどう指定されているのかを分かりやすくするためにUV値を視覚化するとこうなります。
UVの値を色として表示しています。
ただそれだけだとなめらかすぎてわかりにくいので10段階に階層化しました。
こうしてみると三角ポリゴンの境目でUの値がくっきりしているのが分かります。
それに対してVの値は特に問題なさそうです。
この原因は単純です。UVの補間が三角ポリゴン単位で行われているからです。
アプローチ
これを解決するためにアプローチを考えます。
真ん中に頂点足してポリゴン数増やす
ポリゴンを細かくする事でごまかすことが出来るのでは…?というアプローチです。
対角線の交わる点が中心(0.5,0.5)になるはずなので、そこに頂点を足してみます。
はい。なんとなくマシになった感はありますが、やはり違和感バリバリです。
もっと頂点増やしまくれば改善されるかもしれませんが、イタチごっこ感が否めません。
この案は没です。
没なのでソースも略。
UVをフラグメントシェーダーで計算する
2個目のアプローチですがいきなり本命です。
ラスタライザがUVをいい感じに補間してくれないなら自分でフラグメントシェーダーで計算してやればいいというわけです。
どう計算するか
さてシェーダーでやるにしても計算方法が必要ですがそこまで難しくはありません。
求めるものは台形内の任意の点pに対する「線分ep」と「線分ef」の長さが分かればいいのです。
これが分かれば、「ep / ef」でUの値になります。
・eの求め方
eの求め方は「Ae」と「Ce」の長さの比が分かれば求められます。
これはとても簡単です。
補助線を足しました。
「直角三角形ACg」と「直角三角形Aeh」は相似です。なのでこの2つの三角形の各辺の比は同一です。
このことから Ae:AC=Ah:Ag となります。
AgとAhの比は計算するまでもなくわかります。g~Aを0~1としたときに
Ag:Ah = 1:(1-V)
となるからです。(Vの値は下が0で上が1です)
ここから
|AC| * AhのAgに対する比 = |AC| * (1 – V) = |Ae|
が導けます。(|AC|は線分ACの長さです。)
そして|Ae|が分かると
A + normalize(AC) * |Ae| = e
で点eの位置が求められます。(正規化も|AC|ですが、長さとわかりにくいので分けました)
同様に点fの座標を求めると
先ほど書いた通り
|ep| / |fp| = U
としてUが分かります。
シェーダーで実装する
さて前述の計算をシェーダーで実装します。
0から作るのは面倒なので、Unityのビルトインシェーダーの(UI/Default)をベースに実装してます。
まず、計算するためには頂点ABCDと頂点pの座標が必要です。
ABCDは常に固定なので、シェーダー定数としてマテリアルに設定します。
頂点pはラスタライザで補間してもらいたいので、頂点情報に付与します。
それを踏まえてC#側は以下のように
using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; public class TrapezoidCorrection2 : Graphic { [SerializeField] private Texture2D texture_; public override Texture mainTexture { get { return texture_ ?? base.mainTexture; } } protected override void OnPopulateMesh( Mesh mesh ) { // 左下 UIVertex lb = UIVertex.simpleVert; lb.position = new Vector3( -100, 0, 0 ); lb.color = Color.white; lb.uv0 = new Vector2( 0, 0 ); lb.uv1 = lb.position; // 右下 UIVertex rb = UIVertex.simpleVert; rb.position = new Vector3( 100, 0, 0 ); rb.color = Color.white; rb.uv0 = new Vector2( 1, 0 ); rb.uv1 = rb.position; // 右上 UIVertex rt = UIVertex.simpleVert; rt.position = new Vector3( 50, 100, 0 ); rt.color = Color.white; rt.uv0 = new Vector2( 1, 1 ); rt.uv1 = rt.position; // 左上 UIVertex lt = UIVertex.simpleVert; lt.position = new Vector3( -50, 100, 0 ); lt.color = Color.white; lt.uv0 = new Vector2( 0, 1 ); lt.uv1 = lt.position; m_Material.SetVector( "_LT", lt.position ); // lt m_Material.SetVector( "_RT", rt.position ); // rt m_Material.SetVector( "_LB", lb.position ); // lb m_Material.SetVector( "_RB", rb.position ); // rb using ( var vbo = new VertexHelper() ) { vbo.AddUIVertexQuad( new UIVertex[]{ lt, lb, rb, rt } ); vbo.FillMesh( mesh ); } } }
シェーダー定数_LT、_RT、_LB、_RBに座標を4隅の座標を設定。
各頂点のuv1にも座標を指定して、ラスタライザで補間してもらいます。
※positionの値は親の座標変換等を受けてこの値を取得できなくなりますので使用できません。
シェーダー側は以下の通り。
Shader "Custom/TrapezoidCorrectionShader1" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) _StencilComp ("Stencil Comparison", Float) = 8 _Stencil ("Stencil ID", Float) = 0 _StencilOp ("Stencil Operation", Float) = 0 _StencilWriteMask ("Stencil Write Mask", Float) = 255 _StencilReadMask ("Stencil Read Mask", Float) = 255 _ColorMask ("Color Mask", Float) = 15 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Stencil { Ref [_Stencil] Comp [_StencilComp] Pass [_StencilOp] ReadMask [_StencilReadMask] WriteMask [_StencilWriteMask] } Cull Off Lighting Off ZWrite Off ZTest [unity_GUIZTestMode] Blend SrcAlpha OneMinusSrcAlpha ColorMask [_ColorMask] Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "UnityUI.cginc" struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; float2 position : TEXCOORD1; }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; half2 texcoord : TEXCOORD0; float4 worldPosition : TEXCOORD1; float2 position : TEXCOORD2; }; fixed4 _Color; fixed4 _TextureSampleAdd; bool _UseClipRect; float4 _ClipRect; bool _UseAlphaClip; float4 _LT; float4 _RT; float4 _LB; float4 _RB; v2f vert(appdata_t IN) { v2f OUT; OUT.worldPosition = IN.vertex; OUT.vertex = mul(UNITY_MATRIX_MVP, OUT.worldPosition); OUT.texcoord = IN.texcoord; #ifdef UNITY_HALF_TEXEL_OFFSET OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1); #endif OUT.color = IN.color * _Color; OUT.position = IN.position; return OUT; } sampler2D _MainTex; fixed4 frag(v2f IN) : SV_Target { // ここからUの計算 float4 lt = _LT; float4 rt = _RT; float4 lb = _LB; float4 rb = _RB; float2 texcoord = IN.texcoord.xy; float y = 1 - texcoord.y; // 左辺の点を求める // V[LT-LB] float left = (lb.x - lt.x) * y + lt.x; // 右辺の点を求める // V[RT-RB] float right = (rb.x - rt.x) * y + rt.x; left = (IN.position.x - left); right = (right - IN.position.x); float x = left / (left + right); texcoord.x = x; // ここまで half4 color = (tex2D(_MainTex, texcoord) + _TextureSampleAdd) * IN.color; if (_UseClipRect) color *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); if (_UseAlphaClip) clip (color.a - 0.001); return color; } ENDCG } } }
ほぼUI/Defaultのままです。
fragのここから~ここまでというコメントまでが前述の計算です。
シェーダー定数を足しているのと、頂点情報にUV1の追加も行っています。
結果がこうです。
ちゃんと台形になってますね!
応用編1:奥行きを出す
台形にすると奥行き(奥が小さく、手前が大きく)を出したくなります。
アプローチ1:Vを累乗する
Vを累乗するとVの変化が直線から曲線になってそれっぽくなるかもということでシェーダーでVを2乗してみることに。
そこはかとない違和感が…
1.3乗にしてみる。
奥行き感があんまり…
アプローチ2:射影変換する
奥行きが出したいなら通常の3Dと同じように射影変換行列を適用すれば良いのでは…という事で試しました。
良い感じかもしれません。
計算は射影変換行列との積の内必要な分だけ抜き出しました。
Shader "Custom/TrapezoidCorrectionShader1-3" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) _StencilComp ("Stencil Comparison", Float) = 8 _Stencil ("Stencil ID", Float) = 0 _StencilOp ("Stencil Operation", Float) = 0 _StencilWriteMask ("Stencil Write Mask", Float) = 255 _StencilReadMask ("Stencil Read Mask", Float) = 255 _ColorMask ("Color Mask", Float) = 15 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Stencil { Ref [_Stencil] Comp [_StencilComp] Pass [_StencilOp] ReadMask [_StencilReadMask] WriteMask [_StencilWriteMask] } Cull Off Lighting Off ZWrite Off ZTest [unity_GUIZTestMode] Blend SrcAlpha OneMinusSrcAlpha ColorMask [_ColorMask] Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "UnityUI.cginc" struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; float2 position : TEXCOORD1; }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; half2 texcoord : TEXCOORD0; float4 worldPosition : TEXCOORD1; float2 position : TEXCOORD2; }; fixed4 _Color; fixed4 _TextureSampleAdd; bool _UseClipRect; float4 _ClipRect; bool _UseAlphaClip; float4 _LT; float4 _RT; float4 _LB; float4 _RB; v2f vert(appdata_t IN) { v2f OUT; OUT.worldPosition = IN.vertex; OUT.vertex = mul(UNITY_MATRIX_MVP, OUT.worldPosition); OUT.texcoord = IN.texcoord; #ifdef UNITY_HALF_TEXEL_OFFSET OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1); #endif OUT.color = IN.color * _Color; OUT.position = IN.position; return OUT; } /** * プロジェクトション行列の積をZ以外を定数としてyを計算し、 * 0~1の間に正規化する関数 * 変換元座標は[x, y, z] = [0, 1, z]として計算 * @param inZ 0~1のZ値(UVのyを指定) * @return 計算結果 */ float ease( float inZ ) { // プロジェクション行列の変換結果のyの項をz値を元に計算する // -1.1682333052は適当な定数(=cot(画角)) // 1.001001001も適当な定数(=zf/(zf-zn)) float fov = -1.1682333052; float z = 1.001001001 * (1 - inZ) + 1; float invFov = 1 / fov; float y = (fov / z) * invFov; // 0~1に正規化 // zが1のときの最大値 float maxValue = (fov / (1.001001001 + 1)) * invFov; // 最小値を0に float c = y - maxValue; // 最大値を1に return c / (1 - maxValue); } sampler2D _MainTex; fixed4 frag(v2f IN) : SV_Target { float4 lt = _LT; float4 rt = _RT; float4 lb = _LB; float4 rb = _RB; float2 texcoord = IN.texcoord.xy; float y = 1 - texcoord.y; texcoord.y = ease( texcoord.y ); // 左辺の点を求める // V[LT-LB] float left = (lb.x - lt.x) * y + lt.x; // 右辺の点を求める // V[RT-RB] float right = (rb.x - rt.x) * y + rt.x; left = (IN.position.x - left); right = (right - IN.position.x); float x = left / (left + right); texcoord.x = x; half4 color = (tex2D(_MainTex, texcoord) + _TextureSampleAdd) * IN.color; if (_UseClipRect) color *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); if (_UseAlphaClip) clip (color.a - 0.001); //float x1 = texcoord.x; //color = fixed4( x1, x1, x1, 1 ); return color; } ENDCG } } }
easeという関数で射影変換を計算しています。
射影変換行列はD3DXMatrixPerspectiveFovLHを使っています。
応用編2:Spriteを台形にする
今まで書いてきた内容はUVが0~1のテクスチャを1枚そのまま出すのを想定しています。
これを任意の矩形で切り抜いたSprite向けに変更します。
と言ってもこれも難しくはありません。
上記の0~1のときの計算結果を本来のUV値に乗算するだけです。
本来のUV値が必要になるので、UV座標をシェーダー定数で送ります。
using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; public class TrapezoidCorrection2_2 : Graphic { [SerializeField] private Sprite sprite_; public override Texture mainTexture { get { return sprite_ ? (sprite_.texture ?? base.mainTexture) : base.mainTexture; } } private Vector4 getRect() { if ( sprite_ ) { float w = 1f / sprite_.texture.width; float h = 1f / sprite_.texture.height; return new Vector4( sprite_.textureRect.xMin * w, sprite_.textureRect.yMin * h, sprite_.textureRect.xMax * w, sprite_.textureRect.yMax * h ); } return new Vector4( 0, 0, 1, 1 ); } protected override void OnPopulateMesh( Mesh mesh ) { // 左下 UIVertex lb = UIVertex.simpleVert; lb.position = new Vector3( -100, 0, 0 ); lb.color = Color.white; lb.uv0 = new Vector2( 0, 0 ); lb.uv1 = lb.position; // 右下 UIVertex rb = UIVertex.simpleVert; rb.position = new Vector3( 100, 0, 0 ); rb.color = Color.white; rb.uv0 = new Vector2( 1, 0 ); rb.uv1 = rb.position; // 右上 UIVertex rt = UIVertex.simpleVert; rt.position = new Vector3( 50, 100, 0 ); rt.color = Color.white; rt.uv0 = new Vector2( 1, 1 ); rt.uv1 = rt.position; // 左上 UIVertex lt = UIVertex.simpleVert; lt.position = new Vector3( -50, 100, 0 ); lt.color = Color.white; lt.uv0 = new Vector2( 0, 1 ); lt.uv1 = lt.position; Vector4 rect = getRect(); m_Material.SetVector( "_LT", new Vector4( lt.position.x, lt.position.y ) ); // lt m_Material.SetVector( "_RT", new Vector4( rt.position.x, rt.position.y ) ); // rt m_Material.SetVector( "_LB", new Vector4( lb.position.x, lb.position.y ) ); // lb m_Material.SetVector( "_RB", new Vector4( rb.position.x, rb.position.y ) ); // rb m_Material.SetVector( "_STUV", rect ); using ( var vbo = new VertexHelper() ) { vbo.AddUIVertexQuad( new UIVertex[]{ lt, lb, rb, rt } ); vbo.FillMesh( mesh ); } } }
_STUVにUV座標が入ってます。
シェーダー側でこれに0~1のときの結果をかけます。
Shader "Custom/TrapezoidCorrectionShader1-4" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) _StencilComp ("Stencil Comparison", Float) = 8 _Stencil ("Stencil ID", Float) = 0 _StencilOp ("Stencil Operation", Float) = 0 _StencilWriteMask ("Stencil Write Mask", Float) = 255 _StencilReadMask ("Stencil Read Mask", Float) = 255 _ColorMask ("Color Mask", Float) = 15 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Stencil { Ref [_Stencil] Comp [_StencilComp] Pass [_StencilOp] ReadMask [_StencilReadMask] WriteMask [_StencilWriteMask] } Cull Off Lighting Off ZWrite Off ZTest [unity_GUIZTestMode] Blend SrcAlpha OneMinusSrcAlpha ColorMask [_ColorMask] Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "UnityUI.cginc" struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; float2 position : TEXCOORD1; }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; half2 texcoord : TEXCOORD0; float4 worldPosition : TEXCOORD1; float2 position : TEXCOORD2; }; fixed4 _Color; fixed4 _TextureSampleAdd; bool _UseClipRect; float4 _ClipRect; bool _UseAlphaClip; float4 _LT; float4 _RT; float4 _LB; float4 _RB; float4 _STUV; v2f vert(appdata_t IN) { v2f OUT; OUT.worldPosition = IN.vertex; OUT.vertex = mul(UNITY_MATRIX_MVP, OUT.worldPosition); OUT.texcoord = IN.texcoord; #ifdef UNITY_HALF_TEXEL_OFFSET OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1); #endif OUT.color = IN.color * _Color; OUT.position = IN.position; return OUT; } sampler2D _MainTex; fixed4 frag(v2f IN) : SV_Target { float4 lt = _LT; float4 rt = _RT; float4 lb = _LB; float4 rb = _RB; float2 texcoord = IN.texcoord.xy; float y = 1 - texcoord.y; // 左辺の点を求める // V[LT-LB] float left = (lb.x - lt.x) * y + lt.x; // 右辺の点を求める // V[RT-RB] float right = (rb.x - rt.x) * y + rt.x; left = (IN.position.x - left); right = (right - IN.position.x); float x = left / (left + right); // UVの計算 texcoord = float2( x * (_STUV.z - _STUV.x) + _STUV.x, texcoord.y * (_STUV.w - _STUV.y) + _STUV.y ); half4 color = (tex2D(_MainTex, texcoord) + _TextureSampleAdd) * IN.color; if (_UseClipRect) color *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); if (_UseAlphaClip) clip (color.a - 0.001); return color; } ENDCG } } }
奥行きの補正はないやつです。
UVの計算というコメントを入れています。
こんな感じです。