【Unity】Texture2Dにブラシ（Texture2D）でお絵描きするシステムを考える

# はじめに

- Unityに初めから導入されている機能とStandard Assetsのみで実装する
- 筆者の備忘録的な存在です。記事の内容に間違っている場合があるかもしれません。

そういえばSplatoon3の新情報が解禁されましたね。発売が楽しみで仕方がないです＾＾
Splatoonのオブジェクトにインクを塗る技術に興味があり。それらしいものを再現でいないか考えました。Textureについての知識を深めることが出来た。

# 今回のゴール

<iframe width="560" height="315" src="https://www.youtube.com/embed/3OBo1e9qDI4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

Panelのテクスチャにブラシでお絵描きできるようにする。

# どのようにしてリアルタイム（実行中）にテクスチャに情報を書き込むのか

参考にさせていただいたサイト様　(アクセス日：2021/09/28)
おもちゃラボ：Unityでテクスチャにお絵描きしよう
https://nn-hokuson.hatenablog.com/entry/2016/12/08/200133

## 超ざっくり説明
スクリプト内でテクスチャを生成し、そのテクスチャに情報を書き込む。
内容の書き込みが終了したら、生成したテクスチャをオブジェクトのテクスチャとして代入する。

スクリプト内部でテクスチャを作るという点が盲点であった。（シェーダーでどうにかしてやろうと考えていた）

# スクリプトの書き換え

今回やりたいことは、ブラシ（自分の用意したテクスチャ）でテクスチャにお絵描きをすることだ。そのためスクリプトを書き換える必要がある。

もともと一つだったスクリプトを、メインカメラ側とオブジェクト側の２つに分けて実装する。

## オブジェクト側のスクリプト

オブジェクトに張り付けるスクリプトです。

```C#:Object_Logic.C#
public class Object_Logic : MonoBehaviour
{
    Texture2D drawTexture;
    Color32[] buffer;

    // Start is called before the first frame update
    void Start()
    {
        Texture2D mainTexture = (Texture2D)GetComponent<Renderer>().material.mainTexture;
        Color32[] pixels = mainTexture.GetPixels32();

        buffer = new Color32[pixels.Length];
        pixels.CopyTo(buffer, 0);

        drawTexture = new Texture2D(mainTexture.width, mainTexture.height, TextureFormat.RGBA32, false);
        drawTexture.filterMode = FilterMode.Point;
    }

    public void Draw(Vector2 p, Color32[] brushbuffer, Vector2 brushSize)
    {
        p.x = p.x * drawTexture.width;
        p.y = p.y * drawTexture.height;

        for (int x = 0; x < drawTexture.width; x++)
        {
            for (int y = 0; y < drawTexture.height; y++)
            {
                if ((p.x - (brushSize.x / 2)) < x && x < (p.x + (brushSize.x / 2)) && (p.y - (brushSize.y / 2) < y) && y < (p.y + (brushSize.y / 2)))
                {
                    if (brushbuffer[(int)(x - (p.x - (brushSize.x / 2)) + (int)brushSize.x * (int)(y - (p.y - (brushSize.y / 2))))].a != 0)
                    {
                        buffer.SetValue(brushbuffer[(int)(x - (p.x - (brushSize.x / 2)) + (int)brushSize.x * (int)(y - (p.y - (brushSize.y / 2))))], x + drawTexture.width * y);
                    }
                }
            }
        }

        drawTexture.SetPixels32(buffer);
        drawTexture.Apply();
        GetComponent<Renderer>().material.mainTexture = drawTexture;
    }
}
```

### オブジェクト側のスクリプトの解説

```C#:宣言部分
Texture2D drawTexture;
Color32[] buffer;
```

・Texture2D drawTexture　はオブジェクトのマテリアルに適応させるテクスチャです。最後にこのテクスチャにオブジェクトに適応させるテクスチャの情報を書き込みます。

・Color32[] buffer　はオブジェクトに適応させるテクスチャのピクセル情報を配列にしたものです。この配列の情報を書き換えて、最後にdrawTextureに適応させます。

```C#:Start関数部分
void Start()
{
       Texture2D mainTexture = (Texture2D)GetComponent<Renderer>().material.mainTexture;
       Color32[] pixels = mainTexture.GetPixels32();

       buffer = new Color32[pixels.Length];
       pixels.CopyTo(buffer, 0);

       drawTexture = new Texture2D(mainTexture.width, mainTexture.height, TextureFormat.RGBA32, false);
       drawTexture.filterMode = FilterMode.Point;
}
```

・１行目ではTexture2D型の変数mainTextureにスクリプトがアタッチされているオブジェクトのテクスチャを代入しています。
・２行目ではColor32型の配列pixelsにmainTextureのピクセル情報をぶち込んでいます。
・３行目ではColor32型の配列bufferを初期化しています。
・４行目ではpixelsをbufferにコピーしています。このときコピーをbuffer配列の0番目から開始しています。
・５行目ではdrawTextureを初期化しています。
・６行目ではdrawTextureのフィルターモードを設定しています。

```C#:Draw関数部分
public void Draw(Vector2 p, Color32[] brushbuffer, Vector2 brushSize)
{
    p.x = p.x * drawTexture.width;
    p.y = p.y * drawTexture.height;

    for (int x = 0; x < drawTexture.width; x++)
    {
        for (int y = 0; y < drawTexture.height; y++)
        {
            if ((p.x - (brushSize.x / 2)) < x && x < (p.x + (brushSize.x / 2)) && (p.y - (brushSize.y / 2) < y) && y < (p.y + (brushSize.y / 2)))
            {
                if (brushbuffer[(int)(x - (p.x - (brushSize.x / 2)) + (int)brushSize.x * (int)(y - (p.y - (brushSize.y / 2))))].a != 0)
                {
                    buffer.SetValue(brushbuffer[(int)(x - (p.x - (brushSize.x / 2)) + (int)brushSize.x * (int)(y - (p.y - (brushSize.y / 2))))], x + drawTexture.width * y);
                }
            }
        }
    }
    drawTexture.SetPixels32(buffer);
    drawTexture.Apply();
    GetComponent<Renderer>().material.mainTexture = drawTexture;
}
```
Draw関数はメインカメラ側のスクリプトから引数を受け取ります。

・Draw関数の引数について
・第一引数：Vector2型のpはテクスチャのUV座標を表します。
・第二引数：Color32型の配列brushbufferはブラシ（自分が用意したテクスチャ）のピクセル情報が詰まっています。
・第三引数：Vector2型のbrushSizeはブラシ（自分が用意したテクスチャ）のサイズが格納されています。

```C#:Draw関数部分
p.x = p.x * drawTexture.width;
p.y = p.y * drawTexture.height;
```
ここでは第一引数で受け取ったUV座標をTextureのピクセル座標に変換しています。

```C#:Draw関数部分
for (int x = 0; x < drawTexture.width; x++)
{
    for (int y = 0; y < drawTexture.height; y++)
    {
        if ((p.x - (brushSize.x / 2)) < x && x < (p.x + (brushSize.x / 2)) && (p.y - (brushSize.y / 2) < y) && y < (p.y + (brushSize.y / 2)))
        {
            if (brushbuffer[(int)(x - (p.x - (brushSize.x / 2)) + (int)brushSize.x * (int)(y - (p.y - (brushSize.y / 2))))].a != 0)
            {
                buffer.SetValue(brushbuffer[(int)(x - (p.x - (brushSize.x / 2)) + (int)brushSize.x * (int)(y - (p.y - (brushSize.y / 2))))], x + drawTexture.width * y);
            }
        }
    }
}
```

さて、このお絵描きするシステムで最も重要な部分です。

この二重のfor文はdrawTextureのすべてのピクセル情報を参照するためにあります。

１つ目のif文は捜査しているピクセルがブラシ（自分が用意したテクスチャ）で塗りたい範囲かどうかを判断しています。

また、第一引数で受け取ったUV座標をもとに算出したピクセル座標にブラシ（自分が用意したテクスチャ）の中心が来るように調整しています。そのためコードが複雑になっています。

さらに、ピクセルの情報を格納しているbufferは一次元配列なのでピクセル座標をインデックスに変換する必要があります。（インデックスを返す関数を作ってもよさそう）

２つ目のif文はbufferに書き込む色の不透明度が０ではない時にbufferにbrushbufferの色を書き込むようにしています。あえてColor32型で宣言していた理由はこれです。

そして、buffer.SetValueでbufferへの色の書き込みを行っています。

```C#:Draw関数部分
drawTexture.SetPixels32(buffer);
drawTexture.Apply();
GetComponent<Renderer>().material.mainTexture = drawTexture;
```
・１行目でdrawTextureにbufferの情報を書き込みます。
・２行目でdrawTextureへの変更を適応させます。
・そして３行目でスクリプトがアタッチされているオブジェクトのテクスチャをdrawTextureに変更します。

## メインカメラ側のスクリプト

```C#:Camera_Logic.C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Camera_Logic : MonoBehaviour
{
    Object_Logic object_Logic = null;
    public Texture2D brushTexture;
    public Color brushColor = Color.white;
    public float magnification = 1;
    public Color32[] brushbuffer;

    // Start is called before the first frame update
    void Start()
    {
        if (magnification != 1)
        {
            Vector2 newSize = new Vector2(brushTexture.width * magnification, brushTexture.height * magnification);
            brushTexture = Resize(brushTexture, newSize);
        }

        brushbuffer = brushTexture.GetPixels32();

        for (int i = 0; i < brushbuffer.Length; i++)
        {
            byte gray = (byte)(brushbuffer[i].r * 0.2126 + brushbuffer[i].g * 0.7152 + brushbuffer[i].b * 0.0722);

            brushbuffer[i] = new Color32((byte)(gray * brushColor.r),
                                            (byte)(gray * brushColor.g),
                                            (byte)(gray * brushColor.b),
                                            brushbuffer[i].a);
        }
    }

    Texture2D Resize(Texture2D texture2D, Vector2 newSize)
    {
        RenderTexture rt = new RenderTexture((int)newSize.x, (int)newSize.y, 24);
        RenderTexture.active = rt;
        Graphics.Blit(texture2D, rt);
        Texture2D result = new Texture2D((int)newSize.x, (int)newSize.y);
        result.ReadPixels(new Rect(0, 0, (int)newSize.x, (int)newSize.y), 0, 0);
        result.Apply();
        return result;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;

            if (Physics.Raycast(ray, out hit, 100.0f))
            {
                object_Logic = hit.collider.GetComponent<Object_Logic>();

                if (object_Logic != null)
                {
                    object_Logic.Draw(hit.textureCoord, brushbuffer, new Vector2(brushTexture.width, brushTexture.height));
                }
            }
        }
    }
}
```

### メインカメラ側のスクリプトの解説

```C#:宣言部分
Object_Logic object_Logic = null;
public Texture2D brushTexture;
public Color brushColor = Color.white;
public float magnification = 1;
public Color32[] brushbuffer;
```

・１行目ではObject_Logic内部の関数を参照するために宣言しています。
・２行目のTexture2D型のbrushTextureはその名の通りブラシのテクスチャです。白系色で構成されていると上手くいく時が多いです。
・３行目のColor型のbrushColorはブラシの色です。
・４行目のmagnificationはブラシの拡大率です。整数値が好ましいですが、ぶっちゃけ拡大や縮小を行わないほうが良いです。
・５行目のColor32型の配列brushbufferはブラシのピクセル情報を格納するためのものです。

```C#:Start関数
void Start()
{
    if (magnification != 1)
    {
        Vector2 newSize = new Vector2(brushTexture.width * magnification, brushTexture.height * magnification);
        brushTexture = Resize(brushTexture, newSize);
    }

    brushbuffer = brushTexture.GetPixels32();

    for (int i = 0; i < brushbuffer.Length; i++)
    {
        byte gray = (byte)(brushbuffer[i].r * 0.2126 + brushbuffer[i].g * 0.7152 + brushbuffer[i].b * 0.0722);

        brushbuffer[i] = new Color32((byte)(gray * brushColor.r),
                                        (byte)(gray * brushColor.g),
                                        (byte)(gray * brushColor.b),
                                        brushbuffer[i].a);
    }
}
```

・最初の部分はブラシの縮小/拡大を行います。Resize関数は以下のサイト様のコードをリファクタリングしたものです。（丸投げ）

> 
> Stackoverflow : How to resize a Texture2D using height and width?
> https://stackoverflow.com/questions/56949217/how-to-resize-a-texture2d-using-height-and-width
>

・次の部分ではbrushbufferにbrushTextureのピクセル情報をぶち込んでいます。

・また次のfor文はbrushbufferに格納された情報をグレイスケール化しています。グレイスケール化を行うことでブラシへの着色がスムーズにいきます。

```C#:Update関数部分
void Update()
{
    if (Input.GetMouseButton(0))
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, 100.0f))
        {
            object_Logic = hit.collider.GetComponent<Object_Logic>();

            if (object_Logic != null)
            {
                object_Logic.Draw(hit.textureCoord, brushbuffer, new Vector2(brushTexture.width, brushTexture.height));
            }
        }
    }
}
```

・Update関数ではメインカメラからRayを飛ばしています。UnityのRayにはRayが当たったオブジェクトのテクスチャのUV座標を返してくれる機能が付いています。そして、Rayが当たったオブジェクトにobject_Logicがアタッチされている場合に、オブジェクト側のスクリプトDrawを呼び出しています。



オブジェクトにお絵描きをするのって難しいんですね。このシステムだとPanelにしか塗れないので、３Dオブジェクトにも塗れるようにもう少し考えてみようと思います。

以上ですお疲れさまでした。

#　参考文献

>
> おもちゃラボ : Unityでテクスチャにお絵描きしよう（アクセス日：2021/09/29）
> https://nn-hokuson.hatenablog.com/entry/2016/12/08/200133
>

>
> UnityDOCUMENTION Texture2D（アクセス日：2021/09/29）
> https://docs.unity3d.com/ja/current/ScriptReference/Texture2D.html
>

>
> UnityDOCUMENTION Texture2D.SetPixels32（アクセス日：2021/09/29）
> https://docs.unity3d.com/ja/2018.4/ScriptReference/Texture2D.SetPixels32.html
>

>
> stackoverflow How to resize a Texture2D using height and width?（アクセス日：2021/09/29）
> https://stackoverflow.com/questions/56949217/how-to-resize-a-texture2d-using-height-and-width
>

>
> ゲーム作るためのこと学ぶdevろぐ(’ー’)/ﾁｬﾗﾝ 【Unity-Shader】#05 グレースケール化（アクセス日：2021/09/29）
> https://www.snoopopo.com/entry/2020/09/02/095726
>

>
> グレースケール画像のうんちく（アクセス日：2021/09/29）
> https://qiita.com/yoya/items/96c36b069e74398796f3
>