Shapesでお絵描き入門

# はじめに（どうでもよい話ですが…）
これは、アドベントカレンダー15日目の記事です。
もともと「Addressableの調査とSoundManagerでの利用」という題でした。
今、勉強したいことをテーマにしていた訳なのですが、調べる過程で、以下のような素晴らしい記事に多数出会い、**もう何もすることがないな**と思ったので、急遽、別のネタを探すことになりました。。

https://light11.hatenadiary.com/entry/2020/07/29/202755

・・・そこで、
昨年のAsset Storeの年末セールで購入したものの触っていなかった**「Shapes」**というアセットを調査してみることにしました。描画系のツールのようです。

https://assetstore.unity.com/packages/tools/particles-effects/shapes-173167

# まずは、ドキュメントを読んでみる

アセットを読み込んだあとに現れる**Quick Start Guide.pdf**を読みます。
原文（一部省略あり）と、要約、、というか私のメモを書きます。

## 概要

>This guide is a short version of the longer, more comprehensive online documentation I recommend giving it a read! It’s better than this lil guide~
Shapes has two main ways of drawing, either using Shape Components or Immediate-Mode Drawing

- これはダイジェスト版のドキュメント。もっと充実したWeb版は[こちら](https://acegikmo.com/shapes/docs/)。
- Shapesで描画するには、**Shapeコンポーネント**か、**Immediate-Mode**を使う。

## 方法1. Shapeコンポーネント

>To create shapes in your scene, go to Shapes/Create/...
These Shapes work like any other mesh renderers - you can tweak their parameters in the inspector and move them around your scene with game objects! You can also modify their parameters from scripts using GetComponent<T>, and assigning to all the available properties.

- シーンにオブジェクトを追加する、おなじみの方法。
- インスペクタや、GetComponent<T>で全プロパティを調整できる。

// Shapeコンポーネントを利用した分かりやすい記事が見つかりました。

https://kakovail.info/entry/shapeswithdotween

## 方法2. Immediate-Mode

> Drawing in immediate mode is useful for shapes that are only temporary, procedural, or where you don't want to deal with the overhead of many game objects. Immediate mode is instantaneous - it renders only once in the camera you draw in, so if you want to continuously draw, you draw every frame! This means it's very easy to create dynamic setups where what is being drawn can always change with no overhead of destroying or creating GameObjects

```c#
[ExecuteAlways] public class MyScript : ImmediateModeShapeDrawer {
    public override void DrawShapes( Camera cam ){
        using( Draw.Command( cam ) ){
            // Sets up all static parameters.
            // These are used for all following Draw.Line calls Draw.LineGeometry = LineGeometry.Volumetric3D; Draw.ThicknessSpace = ThicknessSpace.Pixels; Draw.Thickness = 4; // 4px wide
            // draw lines
            Draw.Line( Vector3.zero, Vector3.right, Color.red ); Draw.Line( Vector3.zero, Vector3.up, Color.green ); Draw.Line( Vector3.zero, Vector3.forward, Color.blue );
        }
    }
}
```

> Attach this script to any object, and it will draw three 4px wide 3D lines along each axis at the center of the world, with different colors. Let's break it down:

- **動的に変化する形**を作ったり、**オブジェクトをたくさん出す**ようなときに有効。
- 表示し続けるためには毎フレーム描画する必要がある。

### ImmediateModeShapeDrawer

> This is a helper class you can inherit from, that will make drawing in immediate mode a little bit easier when overriding the DrawShapes() method. This method is called once for every active camera, including the scene view if your script is marked with [ExecuteAlways]. Internally, this is just a wrapper for the camera OnPreRender callback, subscribed to in OnEnable and unsubscribed from in OnDisable. If you don't use this class, then it is highly recommended that you use the appropriate OnPreRender callback for your render pipeline when issuing draw commands

- DrawShapesはカメラの**OnPreRenderコールバック**のタイミングで呼び出される。
- **[ExecuteAlways]**がついているとゲームを実行していなくても描画が確認できる。

＜以下、細々しているので省略＞

## 省略したところのメモ

- GPUを使って描画される。
- using... 以下に描画処理を書くのがおすすめ。
- Draw.ShapeName( [Positioning], [Essentials], [Specials], [Coloring] )の順。
- using... 以下に書かずに呼び出す方法もあるが、GPUが使えず重いし、動かないこともある。
- 動的にポリゴンを作ることもできる。

# 感じたこと

**Immediate-Mode**は、昔、遊びで触っていた、**Processingに似ているな、**と思いました。

https://processing.org/examples/

ProcessingのExamplesを参考にしつつ、Shapesでお絵描きをしてみます。
※目コピなので、移植したわけではありません。

# 作ったもの

## 1. 四角をたくさん出す
（おそらく）2304個の四角が動的に描画されています。**とてもスムーズに動きます！**
**[ExecuteAlways]**がついているので、**ゲームを実行しなくても**値が描画に反映されます。
ゲームでは、フェードイン、フェードアウトの表現に使えるかも？
![Screen Recording 2021-12-16 at 3.03.24.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95460/41e68665-edf3-9ca7-c9d9-4ef92e4494e4.gif)

```c#
using Shapes;
using UnityEngine;

[ExecuteAlways]
public class LineSample_1 : ImmediateModeShapeDrawer
{
    [Range(0f, 2f)] public float baseSize;
    [Range(0f, 0.1f)] public float sizeScale;
    [Range(0f, 10f)] public float cornerRadius;
    void Start()
    {
    }

    public override void DrawShapes(Camera cam)
    {
        var screen = new Vector2(1600, 900);
        var space = 25;
         
        using (Draw.Command(cam))
        {
            for (int x = 0; x <= screen.x; x += space)
            {
                for (int y = 0; y <= screen.y; y += space)
                {
                    var pos = new Vector3(x, y, 0f)
                              - new Vector3(screen.x * 0.5f, screen.y * 0.5f, 0);
                    // --
                    var size = Vector3.Distance(pos, Vector3.zero) * this.sizeScale + this.baseSize;
                    Draw.Rectangle(pos, size, size, this.cornerRadius);
                }
            }
        }
    }
}
```

## 2. ベクトル場
（おそらく）1296個の四角と1296個の線が動的に描画されていますが、**とても滑らか！**
線の長さを変えているだけなのですが透明度が変わったような表現になりました。
※このサンプルはProcessingのExampleに元ネタありません。
![Screen Recording 2021-12-16 at 3.03.24_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95460/d2be3d08-4aef-59c8-59c1-c4d46f7a6459.gif)

```c#
using UnityEngine;
using Shapes;

[ExecuteAlways] public class LineSample_3 : ImmediateModeShapeDrawer
{
    [Range(0f, 1f)] public float lineLength = 10;
    [Range(-1f, 1f)] public float a;
    [Range(-1f, 1f)] public float b;
    void Start()
    {
    }
    
    public override void DrawShapes(Camera cam)
    {
        var screen = new Vector2(900, 900);
        var space = 25;
        
        using (Draw.Command(cam))
        {
            for (int x = 0; x <= screen.x; x += space)
            {
                for (int y = 0; y <= screen.y; y += space)
                {
                    var pos = new Vector3(x, y, 0f)
                              - new Vector3(screen.x * 0.5f, screen.y * 0.5f, 0);
                    Draw.Color = Color.white;
                    Draw.Rectangle(pos, 2, 2);
                    // --
                    Draw.Color = Color.white * 0.75f;
                    Draw.Thickness = 0.8f;
                    var dir = Vector3.Normalize(new Vector3(pos.y * this.a, pos.x * this.b, 0f));
                    var length = Vector3.Distance(pos, Vector3.zero) * this.lineLength;
                    Draw.Line(pos, pos + (dir * length));
                }
            }
        }
    }
}
```

## 3. マウス入力の利用
マウスカーソルの位置に一定間隔で丸を生成します。最大400個。とても滑らかに動きます。
ただの白い丸しか生成していないのに、じわじわ怖くて綺麗です。
前記の2つのサンプルとは異なり、ゲームを実行しています。
![Screen Recording 2021-12-16 at 3.03.24_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95460/78bcd765-683b-0104-f9b7-d27c6bf8311c.gif)

```c#
using System.Collections.Generic;
using Shapes;
using UnityEngine;

public class LineSample_2 : ImmediateModeShapeDrawer
{
    private List<Vector3> poses;
    private readonly int MAX = 400;
    [Range(0.5f, 5f)] public float radiusScale = 1f;

    void Start()
    {
        Draw.Color = Color.white * 0.4f;
        Draw.BlendMode = ShapesBlendMode.Screen;

        this.poses = new List<Vector3>();
    }

    private void FixedUpdate()
    {
        Vector3 mousePos = Input.mousePosition;
        mousePos.z = 10.0f;
        Vector3 v = Camera.main.ScreenToWorldPoint(mousePos);

        this.poses.Add(v);

        if (MAX <= this.poses.Count)
            this.poses.RemoveAt(0);
    }

    public override void DrawShapes(Camera cam)
    {
        using (Draw.Command(cam))
        {
            for (int idx = 0; idx < this.poses.Count; idx++)
            {
                var pos = this.poses[idx];
                var radius = (MAX - (this.poses.Count - idx)) * this.radiusScale;
                Draw.Disc(pos, radius);
            }
        }
    }
}
```
### DrawShapesの呼び出し頻度に注意
ドキュメントの通り**DrawShapes**は、カメラの**OnPreRender**のタイミングで呼ばれるので、環境に影響され、呼び出し頻度が短くなったり長くなったりして**一定になりません。**
今回のようなサンプルでは、呼び出し頻度が変化することで表現に大きな影響が出てしまうため、**マウスの座標を入れる部分のみFixedUpdate内**で実装しました。
ちなみにDrawShapesをOnPreRenderとは別のタイミングで呼び出そうとしたら、画面がチカチカしてしまいました。おそらくOnPreRenderと同じタイミングで画面がリフレッシュされているのでしょう。

# 最後に…
1年間、放置したアセットでしたが、Unityらしくない表現がお手軽にできて驚きです！
（Canvasで使おうとしたものの何も表示できなかったので放置した気がする）=> **Canvas非対応**
**色**や**多角形**など、まだまだ試していない要素も多いので年末年始に試してみようと思います。
また、スマホ等の実機での**実行速度**も気になります。

（Immediate-Modeは）ゲームでは… **特別な演出を作るとき**に使える感じでしょうか？
プロトタイプ向きかなと思ったのですが、デフォルトのオブジェクトを使ったほうがヒエラルキーでの視認性が良いですよね。