モブクリエイターのデジタル的スタディ

勉強の記録・プログラミング(unityなど)

【Unity】アフィン変換+テクスチャ合成をやる2Dシェーダー

概要

メインのテクスチャに、別のテクスチャをアフィン変換で変形して合成するシェーダー

 

Unity ver:2021.3.3

前置き


シェーダーの勉強をしてみよう!と思い立ち、テクスチャに別のテクスチャをシールのように貼れるシェーダーが作れたらいいんじゃないかな、もとい勉強になるんじゃないかなと思い立ち、色々勉強して書いてみました。
なかなか難産でした……。
参考文献はこちら↓
amagamina.jp
qiita.com
qiita.com



アフィン変換とは?


まずアフィン変換とは?です。
やってることとしては単なる行列計算です。

そもそも画像とは概して色情報の二次元配列(行列)ですが、それに特定の行列をかけることで、その色の位置を自在に並べ替えることが出来ます。
平行移動をする行列、拡大縮小をする行列など様々なものがありますが、それらを纏めて実行できるのがアフィン変換です。

ようするに、画像を回転、縮小、移動などの方式で変形できる式を、画像のピクセル座標それぞれにかけることですね。

シェーダー本文

Shader "AddAfin"
{
    
    Properties
    {
  //インスペクターに表示する内容

        [PerRendererData] _MainTex("Texture", 2D) = "white" {}

  //重ねるテクスチャ
        _SubTex("Texture plus", 2D) = "white" {}

  //サブテクスチャの変形パラメータ
        _Rotate("Rotate",Range(-6.28,6.28)) = 0

        _ScaleX("ScaleX",Range(0,2)) = 1
        _ScaleY("ScaleY",Range(0,2)) = 1

        _MoveX("MoveX",Range(-0.5,0.5)) = 0
        _MoveY("MoveY",Range(-0.5,0.5)) = 0

  //回転中心
        _PivotX("PivotX",Range(0,1)) = 0.5
        _PivotY("PivotY",Range(0,1)) = 0.5

  //透明度
        _Blend("Blend",Range(0,1)) = 0.5


    }
        SubShader
        {
            // No culling or depth
            Cull Off ZWrite Off ZTest Always

            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag

                #include "UnityCG.cginc"

                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv0 : TEXCOORD0;

                };

                struct v2f
                {
                    float2 uv0 : TEXCOORD0;
                    float4 vertex : SV_POSITION;

                };


                sampler2D _MainTex;
                sampler2D _SubTex;
                float _Rotate;
                float _MoveX;
                float _MoveY;
                float _ScaleX;
                float _ScaleY;
                float _PivotX;
                float _PivotY;
                float _Blend;

    // TRANSFORM_TEXを使う時に各テクスチャについて _ST を付けたfloat4が必要
                float4 _MainTex_ST;
                float4 _SubTex_ST;

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

                fixed4 frag(v2f i) : SV_Target
                {


                // 平行移動
                float3x3 positionMatrix = {
                    1, 0, 0,
                    0, 1, 0,
                    _MoveX, _MoveY, 1
                };

                // 回転
                float3x3 rotateMatrix = {
                  cos(_Rotate), sin(_Rotate), 0,
                  -sin(_Rotate), cos(_Rotate), 0,
                  0, 0, 1
                };

                // 拡大縮小
                float3x3 scaleMatrix = {
                  1/_ScaleX, 0, 0,
                  0, 1/_ScaleY, 0,
                  0, 0, 1
                };

    //変形をかけるためにサブテクスチャの三次元行列をつくる
                float3 mtx = float3(i.uv0.x, i.uv0.y, 1);

    //ピボットの行列を作る
                float3 pv = float3(_PivotX, _PivotY, 0);


    //変形 順番大事!
    //ピボット分ずらす
                mtx = mtx - pv;

                mtx = mul(mtx, positionMatrix);
                mtx = mul(mtx, scaleMatrix);
                mtx = mul(mtx,rotateMatrix);
                
    //ピボット分戻す
                mtx = mtx + pv;


    // サブテクスチャを二次元に戻す
                float2 u = mtx;


                // テクスチャ生成
                fixed4 main = tex2D(_MainTex, i.uv0);
                fixed4 sub = tex2D(_SubTex, u);

                //透明度に合わせてブレンドパラメータの調整
     //step関数で、sub.aが0より大きい時には0を返し、f=1となるようにする
                float f = 1.0 - step(sub.a, 0);
                _Blend = _Blend * f;

                //メインテクスチャとサブテクスチャのブレンドパラメータに合わせた合成
                fixed4 col = main * (1 - _Blend) + sub * _Blend;

                // 全体として透明な部分の描写をしない
                clip(col.a - 0.1);

                return col;


            }
            ENDCG
        }
        }
        
}


ポイントはピボットの足し引き含む変形の順番です。
間違えると変形結果がおかしくなるかもしれないので注意してください。

また、サブテクスチャの透明度をブレンドパラメータで調整していますが、サブテクスチャが透明な部分を合成する際にはブレンドパラメータを0にしています。

詳細は各ドキュメント等などを確認してもらいたいのですが、簡単に言えば、シェーダーとはピクセルのそれぞれについて呼ばれ、実行されるプログラムです。
そして、メインテクスチャとサブテクスチャを合成する時には、ブレンドパラメータを用いてメインテクスチャとサブテクスチャのそれぞれの色の割合を調整し、掛け合わせています。

サブテクスチャで透明なピクセルでシェーダーが呼ばれたときにもブレンドパラメータが0より大きい値のままで二つのテクスチャを掛け合わそうとすると、サブテクスチャが重ならない部分でもメインテクスチャが不本意に薄い色で描写されてしまいます。
なのでこの処理が必要です。

後書き


応用すればマスクや逆マスクも実装できそうです。
それにしても難しかったです。数学音痴には厳しい…。




おすすめの本です↓


無料相談アリ!プロから学びたい方はこちら↓
TechAcademy [テックアカデミー]

【Unity】Addressables asset systemって結局どう使うんだ

概要


UnityにおけるAddressables asset system を活用した実践的なアセット管理フローの考察。

Unity ver:2021.3.3
Addressables ver:1.19.19


前置き


どうもUnityでスクリプトからアセット管理をするにはAddressablesというものがいいらしいというのは、ググれば一瞬で出てきます。(Resourcesは簡単ですが非推奨らしいですね)

ところが探せど探せど出てくる例文は、一つのアセットを読み込んで、リリース。読み込んで、リリース。私のサーチスキルではそれが限界でした。これだけでゲームは作れまいて。

ということで、コピペ芸はそろそろ卒業して自分でスクリプトを書く努力をと思い至った次第です。

私が思い描いていた理想は以下のようなものです。

  • シーンの初めに必要なアセットを複数読み込む。
  • ゲームのループ中それを保持し続け、必要に応じて呼び出し、操作する。
  • ループの終了時に読み込んだアセットを全てアンロードする。

これらを実装するために、構造体を作り、アセットをリストで管理する方法を考えました。ご意見はじゃんじゃんお待ちしています。

リストの構造


使いたいアセットは全てprefubにしておきます。

まず、prefubを管理するclassを作ります。

今回は二つの構造体を作りました。







~~~ 使用例 ~~~
~~~~~~~~~~~

public class PrefubManage
{

struct ItemInfo{

GameObject Object;
PrehubInfo Info;
AsyncOperationHandle handle;

}

public struct PrefubInfo{

public string Key;//インスタンス化したオブジェクトを呼び出すためのキー
public string AssetAddress;//addressablesのaddress
public float X;
public float Y;

}


}
|


また、プレハブを格納するリストは以下です。

List<ItemInfo> PrefubList = new List<ItemInfo>();


PrefubInfo構造体型の情報リストを、メソッドに渡すことで、class内のPrefubListにインスタンス化したオブジェクトを格納していくようにします。

リストに入れる


まず、渡した複数のプレハブ情報に基づいて、全てのプレハブがロードされインスタンス化されるまで待機したいです。
つまり同期的な実装がしたいということです。

よって、asyncを付けたメソッドの中で、インスタンス化を行うTask型メソッドを繰り返し呼びたいと思います。
forが終了した後、actionなどのデリゲートなりでイベントを発火させると、全てのプレハブのロードが終わった後に処理を続けることが出来ます。

保持の時のミソは、インスタンス化したオブジェクトと、ロードを実行した際にaddressablesが返してくるAsyncOperationHandle型構造体をどちらも保存しておくことです。
こうすることで、後にオブジェクトにアクセスして操作したり、削除することが可能になります。


public async void createPrefubs(List<PrefubInfo> list, Action action)
{

for (var i = 0; i < list.Count; i++)
{

//プレハブを一つ一つ順番にロード、インスタンス化し、その間待機する
await addPrf(list[i]);

}

//デリゲート
action();

}



async Task addPrf(PrefubInfo info)
{

//同期的にロード。まだインスタンス化はされていない
AsyncOperationHandle<GameObject> o = Addressables.LoadAssetAsync<GameObject>(info.AssetAddress);

//待機
await o.Task;

if (o.Result != null)//ロードが出来ていれば、オブジェクトはResultに入っている
{

// ロードしたものをインスタンス化
var obj = UnityEngine.Object.Instantiate(o.Result);

ItemInfo ii = new ItemInfo();
ii.Info = info;
ii.Object = obj;
ii.handle = o;

//リストに追加
PrefubList.Add(ii);

 }
       
 }



~~~ 呼び出し例 ~~~

prefubManage = new prefubManage();

List<prehubInfo> list = new List<prehubInfo>();

list.Add(new PrefubInfo("key", "address", 0,0);

prefubManage.createPrefubs(list, action);


void action(){

Debug.Log("OK");

}

~~~~~~~~~~~~~~~~

リストから取り出す


次にインスタンス化したオブジェクトを操作します。
今回は、PrefubInfoに入れておいたX、Yの項目を使って、オブジェクトの位置を変更してみたいと思います。

初めに、設定したKeyを使ってリストの中からオブジェクトを探し出し、次にそのオブジェクトからgetconponentでtransformやrecttransformを呼び出すといった流れです。

他にも、サイズを変更したり、Buttonにイベントをアタッチしたりなど、様々なことに応用できます。

public GameObject getObj(string key)
{
for (var i = 0; i < PrefubList.Count; i++)
{

if (PrefubList[i].Info.Key == key)
{
return PrefubList[i].Object;
 }

}
return null;
}


public void setPosition(string key, UnityEngine.Vector3 pos)
{

getObj(key).transform.GetComponent<RectTransform>().localPosition = pos;

}


~~~ 呼び出し例 ~~~

setPosition(info.Key, new Vector3(info.X, info.Y, 0f));

~~~~~~~~~~~~~~~~

リストから消す


addressablesでロードしたアセットは、単にオブジェクトをデストロイするだけではなく、リリースが必要です。
リリースを怠ると、次にロードするのが同じアセットだとしてもどんどんメモリに積み重なっていきます。
その後、リストからも削除します。
そのために、ハンドルを取得するメソッドと、リストのインデックスを取得するメソッドを作成します。

インスタンス化から削除までの間、

window -> asset management -> Addressables -> event viewer

でメモリの使用状況などチェックしてみるといいと思います。
なお、event viewerでaddressablesのオブジェクトをチェックするためには、

window -> asset management -> Addressables -> setting

などでAddressableAssetSettings.assetに飛び、

Diagnostics -> Send Profiler Events

を有効化する必要があります。

public void removePrefub(string key)
{

GameObject.Destroy(getObj(key));
Addressables.Release(getHandle(key));

PrefubList.RemoveAt(getIndex(key));

}


int getIndex(string key)
{

for (var i = 0; i < PrefubList.Count; i++)
{

if (PrefubList[i].Info.Key == key)
{
return i;
}

}

return -1;

}



 AsyncOperationHandle<GameObject> getHandle(string key)
{
for (var i = 0; i < PrefubList.Count; i++)
{

if (PrefubList[i].Info.Key == key)
{
return PrefubList[i].handle;
}

}

return new AsyncOperationHandle<GameObject>();

}

後書き


ひとまず直感的に組んでみたスクリプトです。
何か穴がありそうな気もしますが、今のところはいい感じに動いてます。







おすすめの本です↓


無料相談アリ!プロから学びたい方はこちら↓
TechAcademy [テックアカデミー]

初めまして

このブログは、プログラミング絶賛勉強中の筆者による勉強の記録です。

 

勉強はただの趣味ですが、ゲームを作ることが目標です。

独学の為ペースも遅く、至らないことも多いかと思いますが、知見を共有できればと考えております。

 

コードの使用は自己責任でお願いします。

何かご指摘があればコメントしていただければ今後の糧になります。

 

なお筆者はこしあん派です。

よろしくお願いいたします。

 

↓主食です。プログラミングも体が資本!健康に生きましょう!