0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ひとりで完走_C# is GODAdvent Calendar 2024

Day 17

UnityEditor拡張 お絵描き機能を実装する

Last updated at Posted at 2024-12-16

UnityEditor拡張

ご存じの通り、Unityには独自ツールを実装するためのEditor拡張APIがあります。
今回はこれを使ってEditor拡張で動く、お絵描き画面を作ります。
いろいろなところで目にするSetPixel()を使ったものではなく、RenderTextureShader,Graphic.Blit()を使ったお絵描き表現となります。

EditorWindow作成

お絵描きを実際する画面を作ります。
今回はプロジェクトビューでお絵描きをしたい画像を右クリックし、コンテキストメニューからEditorWindowを立ち上げる形で行います。

まず、Assetsフォルダの下にEditorフォルダを作成し、そこにImageEditorExtension.csと名前を付けたスクリプトを作成します。
スクリプトは以下の通りです。

// EditorWindowを継承
public class ImageEditorExtension : EditorWindow
{
    // プロジェクトビューで選択した画像
    public static Texture2D importImage;

    public Texture2D brushTexture;
    private Rect imageRect;
    private static Material material;
    private static RenderTexture renderTexture;
    private static string paintPath;

    // 右クリックメニューでプログラムが実行されるようにします。
    // 右クリックからCustom→My Image Editorを選択すればこのプログラムが実行される
    [MenuItem("Assets/Custom/My Image Editor")]
    public static void SetSelectedObject()
    {
        // Selection.activeObjectで右クリックしたアイテムを取得できます。
        // (選択するアイテムの種類に合わせてキャストを忘れず)
        importImage = Selection.activeObject as Texture2D;
        paintPath = AssetDatabase.GetAssetPath(importImage);
        // 選択したアイテムが取得出来たらウィンドウを立ち上げるようにします。
        if(importImage != null)
        {
            ShowWindow();
        }
    }
    // この関数で新しいウィンドウが立ち上がります。
    public static void ShowWindow()
    {
        // ウィンドウが立ち上がったら初期設定
        // Blit()するためのマテリアルをAssetDatabase.LoadAssetAtPath()から取得。パスはAssets以降を記述
        material = AssetDatabase.LoadAssetAtPath<Material>("Assets/PaintMat.mat");
        // 取得した画像をRenderTextureに変換する。
        renderTexture = SetRenderTexture(importImage);
        // Windowの名前を設定
        GetWindow<ImageEditorExtension>("My Image Editor");
    }

    // GUIを表示するための関数自動で呼び出される
    private void OnGUI()
    {
        // お絵描きをするブラシを決めるエリアを表示
        // インスペクターのようにドロップダウンからプロジェクト内の指定したアイテムを読み取れる
        brushTexture = (Texture2D)EditorGUILayout.ObjectField("Brush Texture",brushTexture, typeof(Texture2D), false);

        // この時点ではマテリアルにブラシテクスチャーが反映されていない
        bool isBrushSet = false;

        // ブラシテクスチャーをマテリアルに反映させる。
        if(brushTexture != null || !isBrushSet)
        {
            material.SetTexture("_BrushTex", brushTexture);
            isBrushSet = true;
        }

        // RenderTextureを読み取っていれば、Windowに画像が表示される
        if (renderTexture != null)
        {
            // 画像が読み込まれていればセーブボタンを表示
            if (GUILayout.Button("Save"))
            {
                // 画像のセーブ用メソッドが呼び出される
                // 内容は後に説明
                // SaveRenderTextureToPNG(paintPath, renderTexture);
            }
            // 表示する画像の大きさを指定
            imageRect = GUILayoutUtility.GetRect(renderTexture.width, renderTexture.height, GUILayout.ExpandWidth(false));
            // この関数により画像が表示される
            EditorGUI.DrawPreviewTexture(imageRect, renderTexture);            

        }
    }
    // Texture2DをRenderTextureに変更する
    private static RenderTexture SetRenderTexture(Texture2D baseTexture)
    {
        var rt = new RenderTexture(baseTexture.width, baseTexture.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
        rt.filterMode = baseTexture.filterMode;
        Graphics.Blit(baseTexture, rt);
        return rt;
    }
}

これでひとまず拡張ウィンドウ内に画像が表示されるようになりました。

描画処理

それでは描画処理を書いていきます。
この描画はGraphic.Blit()という現在のマテリアルを適用してテクスチャを上書きするAPIを使って描画します。
マテリアルを作るのにShaderがいるので用意します。
以下がペイント用のShaderです。

Shader"Custom/PaintShader"
{
    Properties
    {
        // スクリプトからセットするのでいらないですが一応記述
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _BrushTex ("Brush (RGB)", 2D) = "white" {}
        _BrushUV ("Brush UV", Vector) = (0, 0, 0, 0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
            // スクリプトから与えられる値
            sampler2D _MainTex;
            sampler2D _BrushTex;
            float4 _BrushUV;

            // 汎用的処理
            v2f vert(appdata_t v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // テクスチャの取得
                fixed4 mainTex = tex2D(_MainTex, i.uv);
                // ブラシのUVを画像の中心に合わせる
                float2 buv = float2(_BrushUV.x -  0.5/_BrushUV.z, _BrushUV.y - 0.5/_BrushUV.w);

                // 与えられたUVから描画する範囲を確定
                // clamp(x,0,1.0)によって繰り返しをなくす
                // _BrushUV.zwは縦横の画像の大きさ(スケール)大きい値程ペイントする領域が小さくなる
                float2 brushUV = clamp((i.uv - buv) * _BrushUV.zw, 0.0, 1.0);

                // 算出されたUVからブラシ上書き用のテクスチャを付ける
                fixed4 brushTex = tex2D(_BrushTex, brushUV);

                // ブラシのAlphaが0のところは元画像のまま、Alphaが1の時はブラシ画像で上書き
                return mainTex * (1.0 - brushTex.a) + brushTex * brushTex.a;
            }
            ENDCG
        }
    }
FallBack"Diffuse"
}

これでひとまずお絵描き用のシェーダーが完成します。
このシェーダーを適用したマテリアルを新規に作成して、上記AssetDatabase.LoadAssetAtPath()に指定します。

この作成したマテリアルでBlit()してお絵描きをするコードが以下になります。
ImageEditorExtension.csに追記していきます。


public static Texture2D importImage;

public Texture2D brushTexture;
private Rect imageRect;
private static Material material;
private static RenderTexture renderTexture;
private static string paintPath;
+private Vector2 mousePosition; //マウスドラッグで取得したポイントをキャッシュする

private void OnGUI()
{
    if (renderTexture != null)
    {
        if (GUILayout.Button("Save"))
        {
            SaveRenderTextureToPNG(paintPath, renderTexture);
        }
        imageRect = GUILayoutUtility.GetRect(renderTexture.width, renderTexture.height, GUILayout.ExpandWidth(false));
        EditorGUI.DrawPreviewTexture(imageRect, renderTexture);            
// マウスドラッグでマウスの場所を取得する
+        Event e = Event.current;
+        if (e.type == EventType.MouseDrag)
+        {
+            mousePosition = e.mousePosition;
+            Repaint();
+        }
+    }
}
// Editor拡張でもアップデートが呼べる
+private void Update()
+{
+    if (renderTexture != null && brushTexture != null)
+    {
+        // クリック位置が画像の領域内か確認
+        if (imageRect.Contains(mousePosition))
+        {
+            // 画像のUVを取得
+            float u = (mousePosition.x - imageRect.x) / imageRect.width;
+            float v = 1-(mousePosition.y - imageRect.y) / imageRect.height;
+            // Blit用マテリアルに算出したUVを渡す
+            material.SetVector("_BrushUV", new Vector4(u, v, 2f, 2f));
+            // お絵描き処理
+            PaintRenderTexture();
+        }
+    }
+ }
// Blitしてマテリアルの内容を上書きしてお絵描きを表現
+ private void PaintRenderTexture()
+{
+   // 白紙の入れ物を作る
+   var renderTextureBuffer = RenderTexture.GetTemporary(renderTexture.width, renderTexture.height);
+   // 白紙の入れ物に現在の画像にマテリアルを適用したものをコピー(これでrenderTextureBufferにはお絵描きされた図が入る)
+   Graphics.Blit (renderTexture, renderTextureBuffer, material);
+   // お絵描きされた絵を元のレンダーテクスチャーに移し替える(表示しているのはこっちなので)
+   Graphics.Blit (renderTextureBuffer, renderTexture);        
+}
// saveButtonを押したらRenderTextureをpngに変えて保存
+private void SaveRenderTextureToPNG(string paintPath, RenderTexture renderTexture)
+{
+    // 画像を取得したときにパスを取得していたので、そのパスを指定する(上書き保存をするイメージ)
+    string path = paintPath;
+    if (path.Length != 0)
+    {
+        // RenderTextureをTexture2Dに変換    
+        var newTex = new Texture2D(renderTexture.width, renderTexture.height);
+        RenderTexture.active = renderTexture;
+        newTex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
+        newTex.Apply();
+
+        // Texture2Dをpngに変換
+        byte[] pngData = newTex.EncodeToPNG();
+        if (pngData != null)
+        {
+            // pngDataを指定したパスに書き込む(保存)        
+            File.WriteAllBytes(path, pngData);
+            // リフレッシュ
+            AssetDatabase.Refresh();
+        }
+    }
+}

これで指定した画像に指定したブラシで描けるようになり、画像の保存もできるようになりました。

注意

ブラシに使う画像ですが、4辺の縁がAlpha=0の透明になってないとおかしくなるので、ブラシ選びには注意してください。 (パーティクルとかで使う丸い円の画像なら大丈夫)

Blit()について

Blit()はグラフィックAPIでGPUを使用するので、SetPixelよりはるかに高速で実装できます。
また、マテリアルの描画を含めた画像で上書きできるので自由度が高いです。
なにかTexture2Dを元に画像を加工する場合は一度RenderTextureに変換してShaderを使って画像を加工するBlit()を使った方が上級者ぶれるのでお勧めです。

次回について

本当はこの記事で書きたかったですが、メッシュからUVを取得してそいつを上にレイヤーとして重ねるようにする機能も作りました。
そのUV表示については次回やるので楽しみにしてください。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?