2
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?

More than 3 years have passed since last update.

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

Posted at

はじめに

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

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

今回のゴール

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

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

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

超ざっくり説明

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

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

スクリプトの書き換え

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

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

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

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

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;
    }
}

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

宣言部分
Texture2D drawTexture;
Color32[] buffer;

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

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

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;
}

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

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はブラシ(自分が用意したテクスチャ)のサイズが格納されています。

Draw関数部分
p.x = p.x * drawTexture.width;
p.y = p.y * drawTexture.height;

ここでは第一引数で受け取ったUV座標をTextureのピクセル座標に変換しています。

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のすべてのピクセル情報を参照するためにあります。

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

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

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

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

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

Draw関数部分
drawTexture.SetPixels32(buffer);
drawTexture.Apply();
GetComponent<Renderer>().material.mainTexture = drawTexture;

・1行目でdrawTextureにbufferの情報を書き込みます。
・2行目でdrawTextureへの変更を適応させます。
・そして3行目でスクリプトがアタッチされているオブジェクトのテクスチャをdrawTextureに変更します。

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

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));
                }
            }
        }
    }
}

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

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

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

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に格納された情報をグレイスケール化しています。グレイスケール化を行うことでブラシへの着色がスムーズにいきます。

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にしか塗れないので、3Dオブジェクトにも塗れるようにもう少し考えてみようと思います。

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

# 参考文献

おもちゃラボ : 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

2
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
2
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?