はじめに
- Unityに初めから導入されている機能とStandard Assetsのみで実装する
- 筆者の備忘録的な存在です。記事の内容に間違っている場合があるかもしれません。
そういえばSplatoon3の新情報が解禁されましたね。発売が楽しみで仕方がないです^^
Splatoonのオブジェクトにインクを塗る技術に興味があり。それらしいものを再現でいないか考えました。Textureについての知識を深めることが出来た。
今回のゴール
Panelのテクスチャにブラシでお絵描きできるようにする。
どのようにしてリアルタイム(実行中)にテクスチャに情報を書き込むのか
参考にさせていただいたサイト様 (アクセス日:2021/09/28)
おもちゃラボ:Unityでテクスチャにお絵描きしよう
https://nn-hokuson.hatenablog.com/entry/2016/12/08/200133
超ざっくり説明
スクリプト内でテクスチャを生成し、そのテクスチャに情報を書き込む。
内容の書き込みが終了したら、生成したテクスチャをオブジェクトのテクスチャとして代入する。
スクリプト内部でテクスチャを作るという点が盲点であった。(シェーダーでどうにかしてやろうと考えていた)
スクリプトの書き換え
今回やりたいことは、ブラシ(自分の用意したテクスチャ)でテクスチャにお絵描きをすることだ。そのためスクリプトを書き換える必要がある。
もともと一つだったスクリプトを、メインカメラ側とオブジェクト側の2つに分けて実装する。
オブジェクト側のスクリプト
オブジェクトに張り付けるスクリプトです。
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に適応させます。
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のフィルターモードを設定しています。
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はブラシ(自分が用意したテクスチャ)のサイズが格納されています。
p.x = p.x * drawTexture.width;
p.y = p.y * drawTexture.height;
ここでは第一引数で受け取ったUV座標をTextureのピクセル座標に変換しています。
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への色の書き込みを行っています。
drawTexture.SetPixels32(buffer);
drawTexture.Apply();
GetComponent<Renderer>().material.mainTexture = drawTexture;
・1行目でdrawTextureにbufferの情報を書き込みます。
・2行目でdrawTextureへの変更を適応させます。
・そして3行目でスクリプトがアタッチされているオブジェクトのテクスチャをdrawTextureに変更します。
メインカメラ側のスクリプト
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はブラシのピクセル情報を格納するためのものです。
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に格納された情報をグレイスケール化しています。グレイスケール化を行うことでブラシへの着色がスムーズにいきます。
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