【Unity】CanvasArtを書けるPainter2Dで座標変換をできるようにしてみた

## はじめに
UnityEditorにはHtmlのいわゆるCanvasに似ている機能があることを知っていますか？
その名も`Painter2D`
実は私自身、この機能を最近まで知りませんでした
以下のような記事を見つけたことがきっかけでした。
https://light11.hatenadiary.com/entry/2024/01/16/195750

使ってみると非常に便利なPainter2D。
私はめちゃくちゃハマりました。

しかし、一つ問題がありました。
座標変換ができないことです。
そこで、座標変換を可能にする実装を考えてみました。

完成品はこちら
https://gist.github.com/desuboruto/bf58c9360dba28c90cbc4e4df42724d7



## Painter2Dとは？
詳しい解説は上で挙げた記事に丸投げしちゃいますが、要するにVisualElement上にベクターグラフィックスの絵を描くことができます。

例えば以下のような表現をUnity上で実現できます。
ちょっとした点の回転だったり
![ffd4e68eb4dd00f012328a1a99068598.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704747/19a5e92c-945b-6d0b-2ed6-b62e6f4d85cf.gif)

魚を泳がせてみたり
![382855aa34ea121478c42dec5b1d18dd.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704747/c509df23-914b-21ed-03e7-6d0e9bf0fb3c.gif)

Painter2Dを使用すればこのような表現を比較的簡単に作れちゃうのです！
今まで四角が基調のUnityで多彩な表現ができると思うとものすごくワクワクしますね！ (従来のUnityでも似た表現ができたかもしれませんが...)

公式ドキュメントを読むとAPIは比較的そろっているように思えます。
https://docs.unity3d.com/ScriptReference/UIElements.Painter2D.html

Painter2D.QuadraticCurveTo()を使用してベジェ曲線が書けることもかなりでかいですね！

このPainter2Dですが、HtmlのいわゆるCanvasと呼ばれているものを参考に開発されたのではないかと勝手に推測しています。
なぜなら、Canvasとできることが似ているからですね。
Canvasの[ドキュメント](https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D#%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9)を読んでみるとかなりPainter2Dに似ていることが分かります。

しかし、Canvasと比較してみるとどうにも不足している要素があります。
今回はその要素の一つである座標変換を実装してみたいと思います。

## 座標変換
当然ですが、描画する際には描画する座標を与えなければなりません。座標があるということは原点があるということで、Painter2Dの原点は左上になります。
この原点を動かすことはPainter2Dの世界の中ではできません。
そこでどうにかしてこの原点を動かしたいなーといったことが始まりでした。

ちなみに座標変換ができると次のような表現が楽になります。

![59a0045e6bd1f8330552727855e2b20c.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704747/2daffda3-7c5b-4fe5-83c2-aa0793103dbd.gif)

この描画物の中心から伸びている枝は複数の長方形の連続からなっています。
平行移動だけならば単純に位置を足していくだけで良かったのですが、これ少しずつ回転しているんですね。
回転移動もふくめて計算を行うと複雑になりいろいろと面倒くさそうです。

こういった回転移動も含めて座標系を考えたい場合に座標変換ができると便利かもしれません。


## 実装
前述のとおり平行移動だけであれば原点に変更量を足していくだけなので以下の実装で事足ります。

```csharp

public sealed class Painter2DWrapper
{
    private readonly Painter2D _painter2D;
    private Vector2 _position;

    public Painter2DScope(Painter2D painter2D)
    {
        _painter2D = painter2D;
    }

    public void Translate(Vector2 value)
    {
        _position += value;
    }
    
    public void BeginPath()
    {
        _painter2D?.BeginPath();
    }

    
    ... Painter2Dの実装
}

```

加えて回転移動もできるようにする場合は、回転行列をかければよさそうです。

:::note info
回転の公式について
![heimenkaiten.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704747/977119b4-a531-96ba-3069-158187e03b6b.png)
https://mathwords.net/heimenkaiten

二次元座標平面上において、(x,y)を原点中心に反時計回りに θ回転させた点の座標 (X,Y)は、以下の式で計算できる。

```math
\begin{equation}
\begin{pmatrix}
X \\
Y
\end{pmatrix}
=
\begin{pmatrix}
\cos\theta & -\sin\theta \\
\sin\theta & \cos\theta
\end{pmatrix}
\begin{pmatrix}
x \\
y
\end{pmatrix}
\end{equation}
```
:::

よって、`Translate()`する際に角度の分だけ回転したベクトルを原点に足してあげればよさそうです。
角度はRotate()の際に加算していくようにします。

```csharp
public sealed class Painter2DWrapper
{
    private readonly Painter2D _painter2D;
    private Vector2 _position;
    private float _rotation;

    public Painter2DWrapper(Painter2D painter2D)
    {
        _painter2D = painter2D;
    }

    public void Rotate(float value)
    {
        _rotation += value;
    }

    public void Translate(Vector2 value)
    {
        _position = Transform(value);
    }

    private Vector2 Transform(Vector2 position)
    {
        var cos = Mathf.Cos(_rotation);
        var sin = Mathf.Sin(_rotation);

        var rotatedX = cos * position.x - sin * position.y;
        var rotatedY = sin * position.x + cos * position.y;

        return new Vector2(rotatedX, rotatedY) + _position;
    }

    public void BeginPath()
    {
        _painter2D?.BeginPath();
    }
    ... Painter2Dの実装
}
```

これで座標変換ができるようになりました。
(Painter2DをラップしているためPainter2Dと同じメソッドを書く必要がある点は設計としてイケてない気もしますが...)

今回は含めませんでしたが、拡大縮小も同じ要領でできるはずです。

最終的にはCanvasのsave()やrestore()にあたる状態の保存も行いたかったため以下のような実装になりました。
https://gist.github.com/desuboruto/bf58c9360dba28c90cbc4e4df42724d7

## 使い方

以下のように使用できます
```csharp
public sealed class TestComponent : VisualElement
{
    public TestComponent()
    {
        generateVisualContent -= OnGenerateVisualContent;
        generateVisualContent += OnGenerateVisualContent;
    }

    public void OnGenerateVisualContent(MeshGenerationContext context)
    {
        var painter = context.painter2D;
        using (var painterScope = new Painter2DScope(painter))
        {
            painterScope.FillColor = Color.red; // 塗りつぶす色を赤に変更
            painterScope.Translate(new Vector2(100f, 100f)); // (100,100) 方向に原点を移動 
            painterScope.Rotate(90f * Mathf.Deg2Rad); // 90°回転

            painterScope.BeginPath(); 
            painterScope.MoveTo(new Vector2(0f, 0f));
            painterScope.LineTo(new Vector2(100f, 100f));
            painterScope.LineTo(new Vector2(0f, 100f));
            painterScope.ClosePath();
            painterScope.Fill();
        }
    }
}
```
これを表示してみると以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704747/06eb01ab-48fa-4d4e-aee1-fbd40d7e1845.png)


原点は(100,100)にあるはずなので、ちょうど三角形の右上の頂点に位置していることになります。

なんだか、もっと増やしたくなってきました。


```diff_c_sharp
public sealed class TestComponent : VisualElement
{
    public TestComponent()
    {
        generateVisualContent -= OnGenerateVisualContent;
        generateVisualContent += OnGenerateVisualContent;
    }

    public void OnGenerateVisualContent(MeshGenerationContext context)
    {
        var painter = context.painter2D;
        using (var painterScope = new Painter2DScope(painter))
        {
            painterScope.FillColor = Color.red; // 塗りつぶす色を赤に変更
            painterScope.Translate(new Vector2(100f, 100f)); // (100,100) 方向に原点を移動 

+           for (var i = 0; i < 4; i++)
+           {
                painterScope.Rotate(90f * Mathf.Deg2Rad); // 90°回転
                painterScope.BeginPath();
                painterScope.MoveTo(new Vector2(0f, 0f));
                painterScope.LineTo(new Vector2(100f, 100f));
                painterScope.LineTo(new Vector2(0f, 100f));
                painterScope.ClosePath();
                painterScope.Fill();
+           }
        }
    }
}
```

これを表示してみると以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704747/50affcd7-f9aa-e8de-a8fd-46a51154dbbb.png)

繰り返すだけで風車が完成するなんて画期的ですね！...?

以上。

## 最後に

Painter2Dにて座標変換をできるようにしてみました。
普段からグローバル座標やらローカル座標を扱っているUnitierからしたら大げさに解説するほどでもなかったかもしれません。
コードも雑なのはわかっていますが、改善案等ありましたらコメントいただけると嬉しいです。
また、記事中ででてくる魚や海藻は[[ネタ]VSCodeで魚を飼って仕事中に癒されることにした](https://zenn.dev/remrem/articles/1927424ef516c5)を参考にしています。

みなさんもよかったらPainter2Dで絵を描いてみてください。