Unity uGUIの平行四辺形への変形をシア―(Shear)変換(せん断)で実装
参考
- CVMLエキスパートガイド: 画像のシアー (shear)・せん断 [画像処理]
- Qiita: 完全に理解するアフィン変換
- Qiita: Unity 回転とスケールだけを用いたShear(せん断)の実装
- ChatGPT: ChatGPT4による実装
- BaseMeshEffectとは?
概要
シア―変換はタイトル通り平行四辺形への変形するための機能として使われます。
変換と言われるように詳しくは数学の行列とTransformが関わりますが割愛します。
PhotoshopやIllustrator, BlenderなどのDCCツールユーザーにはおなじみかもしれないですが、Unityではプログラミングしないと実現しづらいです。
ことの経緯
私が知らないだけなのかもですが、日本語&英語ともに情報がほとんど見受けられないので備忘録として書きます。
またUnrealEngineではデフォルトWidgetBPでシア―変形できるのですが、Unityには無いことに気づき、これくらいデフォルトのuGUIに搭載してください(懇願)という気持ちがありました。
UE4でのシア―変形
なぜつくるのか
- イメージでテキストを作るときわざわざ斜体を用意する必要がなく、またその分容量削減できる。
- スライドアニメーションするときに変形をかけるとかっこいいアニメーションが作れる。
開発環境
Unity 2021.3.9f1 URP
Rider 2023.1.2
Unityのバージョンが古いのは気にしないでください(汗)
検証したのはURPですが、スクリプトもシェーダーも2D向けのAPIしか使っていないので、レンダーパイプラインは気にしなくてよいと思います。
uGUI向けのシンプルなシア―変換スクリプト
ChatGPT4さんに手伝ってもらうと大変素晴らしいコードを吐いてくれました。
BaseMeshEffectコンポーネントを継承しuGUI(2D)のみ対応したエディタ上で変更可能な実装。
uGUIの頂点座標を変更することによってシア―変換する形です。
shearのx,yはそれぞれ横と縦の引き伸ばし処理に対応しています。
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// シンプルなuGUI要のシア―(Shear)変換(せん断)
/// </summary>
[RequireComponent(typeof(Graphic))]
public class SimpleShearMeshEffect : BaseMeshEffect
{
[Tooltip("縦横の倍率: Pivotが変換軸、また反映立xはheightにyはwidthに比例する")]
public Vector2 shear = new Vector2(0.2f, 0.0f);
public override void ModifyMesh(VertexHelper vh)
{
if (!IsActive())
return;
// 頂点数
var vertCount = vh.currentVertCount;
// UIの頂点
UIVertex vert = new UIVertex();
// すべての頂点に対して
for (int i = 0; i < vertCount; i++)
{
vh.PopulateUIVertex(ref vert, i);
var pos = vert.position;
// 頂点のy座標をもとに横にずらす
pos.x += shear.x * pos.y;
// 頂点のx座標をもとに縦にずらす
pos.y += shear.y * pos.x;
// UIに頂点変更値を保存
vert.position = pos;
vh.SetUIVertex(vert, i);
}
}
}
すべての頂点座標(矩形なら最大6つ)の各x, yに対して、
xにはy、yにはxの値をShearのx,yの倍率で加算するかたちです。
/* 省略 */
// 頂点のy座標をもとに横にずらす
pos.x += shear.x * pos.y;
// 頂点のx座標をもとに縦にずらす
pos.y += shear.y * pos.x;
/* 省略 */
BaseMeshEffectとは?
恥ずかしながら初見ですが、uGUIの特にTextを加工するために用いることが多いようです。
特筆すべきは頂点をいじれることだと思います。これは色々応用範囲が広そうです。
参考: UI Textに様々な効果を付け加えるスクリプト「ModifyText」を作ってみた
使い方としては、uGUI(Image, Text, etc.)が追加されているGameObjectにSimpleSearMeshEffect.cs
を追加すればあとはエディタ上でshear
値を変更できます。
ChatGPT4が吐いたShearuGUIの変換
TransformのScaleとRotationの変換を重ね掛けする実装
Qiitaの記事: Unity 回転とスケールだけを用いたShear(せん断)の実装
が非常に参考になります。
大変ありがとうございます。
コードは上記記事とほぼ同じです。
こちらはTransformをシア―変換する変換行列を応用する形です。
こちらは3Dにも対応しているようです。
using UnityEngine;
/// <summary>
/// 3重ネストされたTransformをシア―(Shear)変形(せん断)する
/// <see cref="https://qiita.com/Arihi/items/f17b5df3d75040f1c319"/>
/// </summary>
[ExecuteAlways]
public class NestingTransformShear : MonoBehaviour
{
[Range(-90, 90)] public float phi = 30f;
private void OnValidate()
{
// z軸周りにShearさせる
float angle = 90 - phi;
// 角度数は0から90まで
transform.localRotation = Quaternion.Euler(0.0f, 0.0f, angle * 0.5f);
// 親オブジェクトは常に-45度傾く
transform.parent.localScale =
new Vector3(Mathf.Sin(angle * Mathf.Deg2Rad * 0.5f), Mathf.Cos(angle * Mathf.Deg2Rad * 0.5f), 1.0f);
transform.parent.localRotation = Quaternion.Euler(0, 0, -45);
// 親の親オブジェクト: localScale.yは常に√2の値
transform.parent.parent.localScale =
new Vector3(Mathf.Sqrt(2) / Mathf.Sin(angle * Mathf.Deg2Rad), Mathf.Sqrt(2), 1.0f);
}
}
使い方としては、uGUI(Image, Text, etc.)が追加されているGameObjectに親とそのまた親を追加する必要があります。uGUIが追加されている一番下のオブジェクトにNestingTransformShear.cs
を追加してphi
を変更して編集できます。phi
を-90や90にするとエラーがでるので各自、適切に修正する必要もありそうです。
Unity 回転とスケールだけを用いたShear(せん断)の実装
uGUIシェーダー(UI/Default)ベースの実装
場合によっては頂点シェーダーとして実装したいときもあります。
今回はUIのビルトインシェーダーであるUI/Defaultシェーダーを拡張してみます。
以下の公式のデータアーカイブからダウンロードする必要があります。
※シェーダーはMITライセンス。
UI/Defaultシェーダーについては以下Zenn記事を参考に。
シェーダーから頂点の変更を行う場合も基本の計算式は変わりません。
ただしUI/Defaultシェーダーは頂点の変形後、正しい表示座標に補正(オフセット)しないため、スクリプトから補正値(自身のlocalPosition)を入力する必要があります。
uGUI(つまりSpriteRendererではない)向けのシェーダーとスクリプトを実装してみます。
まず以下はシェーダ例です。
シェーダ例(UI/Custom/Shear): 長いので折りたたまれています
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
Shader "UI/Custom/Shear"
{
Properties
{
// Shear(シア変換)用のプロパティ: 平行四辺形の変形に用いる
[Header(Shear)]
[Toggle(USE_SHEAR_DEGREE)] _UseShearDegree ("Use ShearDegree", float ) = 0 // 角度数に切り替える
_ShearDegree ("ShearDegree", Range(-90,90)) = 0 // 角度数の調整値
_ShearXY ("ShearXY", Vector) = (0,0,0,0) // ただのXY軸の調整値
[PerRendererData] _Position ("Position", Vector) = (0,0,0,0) // スクリプトから入力されるuGUIのRectTransform(Transform)の座標
[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
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
}
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 One OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
#pragma multi_compile_local _ USE_SHEAR_DEGREE // 角度数指定切り替えのプリプロセッサ命令定義
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
half4 mask : TEXCOORD2;
UNITY_VERTEX_OUTPUT_STEREO
};
// Shear(シア変換)の変数
float _ShearDegree;
vector _ShearXY;
float3 _Position;
sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float4 _MainTex_ST;
float _UIMaskSoftnessX;
float _UIMaskSoftnessY;
v2f vert(appdata_t v)
{
// シア変換
#ifdef USE_SHEAR_DEGREE
float shearRadians = radians(_ShearDegree);
_ShearXY.xy = float2(sin(shearRadians), 0);
#endif
// uGUIの場合シア変換した頂点からPosition分オフセットしないとずれる
_Position.y *= -1;
v.vertex.x += (v.vertex.y + _Position.y) * _ShearXY.x;
v.vertex.y += (v.vertex.x + _Position.x) * _ShearXY.y;
// swizzle(ベクトルの成分を入れ替える)を使った記述例
// v.vertex.xy += (v.vertex.yx + _Position.yx) * _ShearXY;
// オフセットしない変形処理: 座標がCanvasの中心から離れるほど表示位置がずれる
// v.vertex.x += v.vertex.y * _ShearXY.x;
// v.vertex.y += v.vertex.x * _ShearXY.y;
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
float4 vPosition = UnityObjectToClipPos(v.vertex);
OUT.worldPosition = v.vertex;
OUT.vertex = vPosition;
float2 pixelSize = vPosition.w;
pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
float2 maskUV = (v.vertex.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
OUT.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
OUT.mask = half4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy)));
OUT.color = v.color * _Color;
return OUT;
}
fixed4 frag(v2f IN) : SV_Target
{
half4 color = IN.color * (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd);
#ifdef UNITY_UI_CLIP_RECT
half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy)) * IN.mask.zw);
color.a *= m.x * m.y;
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
color.rgb *= color.a;
return color;
}
ENDCG
}
}
}
UI/Custom/Shaer
シェーダーは適当なファイルに記述してください。
今回は仮にUI-Custom-Shear.shader
ファイルに保存。
UI/Defaultから追加された行の詳解はコメントに記載したため割愛します。
UI/Defaultから追加された行
追加されたプロパティたち
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
Shader "UI/Custom/Shear"
{
Properties
{
+ // Shear(シア変換)用のプロパティ: 平行四辺形の変形に用いる
+ [Header(Shear)]
+ [Toggle(USE_SHEAR_DEGREE)] _UseShearDegree ("Use ShearDegree", float ) = 0 // 角度数に切り替える
+ _ShearDegree ("ShearDegree", Range(-90,90)) = 0 // 角度数の調整値
+ _ShearXY ("ShearXY", Vector) = (0,0,0,0) // ただのXY軸の調整値
+ [PerRendererData] _Position ("Position", Vector) = (0,0,0,0) // スクリプトから入力されるuGUIのRectTransform(Transform)の座標
/* 省略 */
追加された変数たち
/* 省略 */
SubShader
{
/* 省略 */
Pass
{
/* 省略 */
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
+ #pragma multi_compile_local _ USE_SHEAR_DEGREE // 角度数指定切り替えのプリプロセッサ命令定義
/* 省略 */
// Shear(シア変換)の変数
+ float _ShearDegree;
+ vector _ShearXY;
+ float3 _Position;
sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float4 _MainTex_ST;
float _UIMaskSoftnessX;
float _UIMaskSoftnessY;
/* 省略 */
追加れた頂点シェーダ処理
/* 省略 */
v2f vert(appdata_t v)
{
+ // シア変換
+ #ifdef USE_SHEAR_DEGREE
+ float shearRadians = radians(_ShearDegree);
+ _ShearXY.xy = float2(sin(shearRadians), 0);
+ #endif
+ // uGUIの場合シア変換した頂点からPosition分オフセットしないとずれる
+ _Position.y *= -1;
+ v.vertex.x += (v.vertex.y + _Position.y) * _ShearXY.x;
+ v.vertex.y += (v.vertex.x + _Position.x) * _ShearXY.y;
+ // swizzle(ベクトルの成分を入れ替える)を使った記述例
+ // v.vertex.xy += (v.vertex.yx + _Position.yx) * _ShearXY;
+ // オフセットしない変形処理: 座標がCanvasの中心から離れるほど表示位置がずれる
+ // v.vertex.x += v.vertex.y * _ShearXY.x;
+ // v.vertex.y += v.vertex.x * _ShearXY.y;
/* 省略 */
次に上記のUI/Custom/Shear
を使うためのC#スクリプト例です。
using UnityEngine;
using UnityEngine.UI;
namespace MyProject.Scripts.UI
{
[ExecuteAlways]
public class SimpleUICustomShearRenderer : MonoBehaviour
{
[SerializeField] protected Graphic graphic = null;
public readonly int propertyIdPosition = Shader.PropertyToID("_Position");
// ExecuteAlwaysでOnRendererObjectなら座標変更をリアルタイムに反映する
// ※OnValidateの場合、座標変更を保存するまでシェーダーに反映されない
private void OnRenderObject()
{
ApplyProperties();
}
public void ApplyProperties()
{
if (graphic == null)
{
// 自動でフィールドに入れているが単なる好みなのでこの部分は無くても可
TryGetComponent(out graphic);
}
// シェーダー側の_Positionに自分のローカ座標を渡して頂点をオフセットする
graphic.material.SetVector(propertyIdPosition, transform.localPosition);
}
}
}
使い方としてはuGUI(Image, Text, etc.)が追加されているオブジェクトにUI/Custom/Shear
のマテリアルを付ける。
次にSimpleUICustomShearRenderer.cs
もコンポーネントに追加する。
これで最小限のスクリプト&シェーダーの組み合わせで実現可能です。
- マテリアルの
ShearXY
のXY値の編集で変形できる。 - また
Use ShearDegree
にチェックを入れるとShearDegree
による角度数の変形に切り替る。
頂点シェーダとオフセット用スクリプトによるシア変形
uGUIの頂点変形を補正しない場合
仮に自身の座標を指定してオフセットしない場合、座標がCanvasの中心から離れるほど表示位置がずれます。
つまり先に紹介したSimpleShearMeshEffect.cs
の計算処理をUI/Defaultの頂点シェーダにそのまま記述してはならないということです。
これはもともとUI/Defaultシェーダーが頂点変形を考慮しておらず、補正処理がないためです。
uGUIの各頂点はShear変形によって横はv.vertex.y、縦はv.vertex.xに比例してずれます。
/* 省略 */
// オフセットしない変形処理: 座標がCanvasの中心から離れるほど表示位置がずれる
v.vertex.x += v.vertex.y * _ShearXY.x;
v.vertex.y += v.vertex.x * _ShearXY.y;
/* 省略 */
頂点シェーダでのシア変形をオフセットしないとCanvasの中心から離れるほどズレるの図
スプライト(SpritesRenderer)と頂点シェーダでの実装
uGUIではなくスプライト(SpriteRenderer)を使いたい場合もあります。
今回はSprite/Default
シェーダーの頂点シェーダを拡張してみます。
これも先に紹介した公式のデータアーカイブのビルトインシェーダーに含まれています。
Sprite/Defaultの拡張は以下記事も参考になります。
スプライトは内部的に頂点変形後の座標補正(オフセット)が行われているのか、シア変換後のオフセットは必要ないです。uGUIよりも簡単に実装できそうです。
以下はシェーダー例です。
シェーダー例(Sprites/Custom/Shear): 長いので折りたたまれています
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
Shader "Sprites/Custom/Shear"
{
Properties
{
// Shear(シア変換)用のプロパティ: 平行四辺形の変形に用いる
[Header(Shear)]
[Toggle(USE_SHEAR_DEGREE)] _UseShearDegree ("Use ShearDegree", float ) = 0 // 角度数に切り替える
_ShearDegree ("ShearDegree", Range(-90,90)) = 0 // 角度数の調整値
_ShearXY ("ShearXY", Vector) = (0,0,0,0) // ただのXY軸の調整値
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
[HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
[HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
[PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {}
[PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Cull Off
Lighting Off
ZWrite Off
Blend One OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex SpriteVert_Custom // シア変形を追加した関数に変更
#pragma fragment SpriteFrag
#pragma target 2.0
#pragma multi_compile_instancing
#pragma multi_compile_local _ PIXELSNAP_ON
#pragma multi_compile _ ETC1_EXTERNAL_ALPHA
#pragma multi_compile _ USE_SHEAR_DEGREE // 角度数指定切り替えのプリプロセッサ命令定義
#include "UnitySprites.cginc"
// Shear(シア変換)の変数
float _ShearDegree;
vector _ShearXY;
// Sprite/Default UnitySprite.cgincを拡張
v2f SpriteVert_Custom(appdata_t IN)
{
// シア変換
#ifdef USE_SHEAR_DEGREE
float shearRadians = radians(_ShearDegree);
_ShearXY.xy = float2(sin(shearRadians), 0);
#endif
IN.vertex.xy += IN.vertex.yx * _ShearXY;
v2f OUT;
UNITY_SETUP_INSTANCE_ID (IN);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.vertex = UnityFlipSprite(IN.vertex, _Flip);
OUT.vertex = UnityObjectToClipPos(OUT.vertex);
OUT.texcoord = IN.texcoord;
OUT.color = IN.color * _Color * _RendererColor;
#ifdef PIXELSNAP_ON
OUT.vertex = UnityPixelSnap (OUT.vertex);
#endif
return OUT;
}
ENDCG
}
}
}
使い方としてはSpriteRendererにSprite/Custom/Shear
のマテリアルを付ける。
- マテリアルの
ShearXY
のXY値の編集で変形できる。 - また
Use ShearDegree
にチェックを入れるとShearDegree
による角度数の変形に切り変わる。
Spriteと頂点シェーダベースのシア変形
Sprite/Defaultから変更された行について詳解は割愛します。
注意点としてuGUIのときの自身のローカル座標によるオフセットは必要ないです。
また#pragma vertex
に拡張後の関数名を指定しなおすことが必要です。
変更された行について
追加されたプロパティたち
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
Shader "Sprites/Custom/Shear"
{
Properties
{
+ // Shear(シア変換)用のプロパティ: 平行四辺形の変形に用いる
+ [Header(Shear)]
+ [Toggle(USE_SHEAR_DEGREE)] _UseShearDegree ("Use ShearDegree", float ) = 0 // 角度数に切り替える
+ _ShearDegree ("ShearDegree", Range(-90,90)) = 0 // 角度数の調整値
+ _ShearXY ("ShearXY", Vector) = (0,0,0,0) // ただのXY軸の調整値
/* 省略 */
追加、変更された定義
/* 省略 */
Pass
{
CGPROGRAM
- #pragma vertex SpriteVert
+ #pragma vertex SpriteVert_Custom // シア変形を実装した関数の呼び出しに変更
#pragma fragment SpriteFrag
#pragma target 2.0
#pragma multi_compile_instancing
#pragma multi_compile_local _ PIXELSNAP_ON
#pragma multi_compile _ ETC1_EXTERNAL_ALPHA
+ #pragma multi_compile _ USE_SHEAR_DEGREE // 角度数指定切り替えのプリプロセッサ命令定義
#include "UnitySprites.cginc"
+ // Shear(シア変換)の変数
+ float _ShearDegree;
+ vector _ShearXY;
+ // Sprite/Default UnitySprite.cgincのSpriteVertを拡張
+ v2f SpriteVert_Custom(appdata_t IN)
+ {
+ // シア変換
+ #ifdef USE_SHEAR_DEGREE
+ float shearRadians = radians(_ShearDegree);
+ _ShearXY.xy = float2(sin(shearRadians), 0);
+ #endif
+ IN.vertex.xy += IN.vertex.yx * _ShearXY;
+ // 以下はUnitySprite.cgingのSpriteVert関数の記述
+ v2f OUT;
+ UNITY_SETUP_INSTANCE_ID (IN);
+ UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
+ OUT.vertex = UnityFlipSprite(IN.vertex, _Flip);
+ OUT.vertex = UnityObjectToClipPos(OUT.vertex);
+ OUT.texcoord = IN.texcoord;
+ OUT.color = IN.color * _Color * _RendererColor;
+ #ifdef PIXELSNAP_ON
+ OUT.vertex = UnityPixelSnap (OUT.vertex);
+ #endif
+ return OUT;
+ }
ENDCG
}
}
}
Discussion