テクスチャにマウスで線を描く

# はじめに
Unityの `SpriteRenderer` で表示されるスプライトにマウスで書き込みます。

# 開発環境
````.sh
Unity 2020.3.30
````

# 概要
以下のようにマウスでスプライトに書き込みます。
![画面収録 2022-08-16 23.03.25.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263901/95a3166a-2fd1-a9df-2429-cfdce3a6ea8e.gif)

# 実装

書き込み可能なスプライトを用意する側
````Surface.cs
using System.Linq;
using UnityEngine;
using UniRx;

namespace Study.App.BrushDraw
{
    public class Surface : MonoBehaviour
    {
#pragma warning disable 0649
        [SerializeField] SpriteRenderer m_spriteRenderer;
        [SerializeField] Vector2Int m_canvasSize = new Vector2Int(256, 256);
        [SerializeField] float m_pixelPerUnit = 256;
#pragma warning restore 0649

        void Awake()
        {
            var tex = new Texture2D(m_canvasSize.x, m_canvasSize.y);
            var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), m_pixelPerUnit);
            sprite.texture.SetPixels(Enumerable.Range(0, tex.width * tex.height).Select(_ => Color.white).ToArray());
            sprite.texture.Apply();
            m_spriteRenderer.sprite = sprite;
        }

        public static Vector2Int? WorldToTexturePos(SpriteRenderer spriteRenderer, Vector2 pos)
        {
            var sprite = spriteRenderer.sprite;
            var spritePos = (Vector2)spriteRenderer.transform.position;
            var spriteSize = sprite.bounds.size;
            var spriteHalf = spriteSize / 2.0f;
            var diff = spritePos - pos;
            // ignore if point is outer of sprite.
            if (diff.x < -spriteSize.x / 2f ||
               diff.x >= spriteSize.x / 2f ||
               diff.y < -spriteSize.y / 2f ||
               diff.y >= spriteSize.y / 2f)
            {
                return null;
            }
            // convert world point to texture point.
            var center = new Vector2Int(sprite.texture.width / 2, sprite.texture.height / 2);
            var offset = new Vector2((diff.x / spriteHalf.x) * (sprite.texture.width / 2), (diff.y / spriteHalf.y) * (sprite.texture.height / 2));
            center.x -= Mathf.RoundToInt(offset.x);
            center.y -= Mathf.RoundToInt(offset.y);
            return center;
        }

        public Vector2Int? WorldToTexturePos(Vector2 pos) => WorldToTexturePos(m_spriteRenderer, pos);

        public void FillRect(Vector2Int at, Color col, int size)
        {
            int half = size / 2;
            Put(at, col);
            var pos = Enumerable.Range(-half, size)
                      .SelectMany(i => Enumerable.Range(-half, size).Select(j => new Vector2Int(i, j)));
            foreach (var ofs in pos)
            {
                Put(at + ofs, col);
            }
        }

        public void Put(Vector2Int at, Color col)
        {
            var tex = m_spriteRenderer.sprite.texture;
            if (at.x >= 0 && at.x < tex.width && at.y >= 0 && at.y < tex.height)
            {
                tex.SetPixel(at.x, at.y, col);
            }
        }

        public void Apply() => m_spriteRenderer.sprite.texture.Apply();
    }
}
````


マウスの動きを検知して書き込む側
````Brush.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.InputSystem;
using UniRx;

namespace Study.App.BrushDraw
{
    public class Brush : MonoBehaviour
    {
#pragma warning disable 0649
        [SerializeField] Camera m_camera;
        [SerializeField] Surface m_surface;
        [SerializeField] Color m_color = Color.red;
        [SerializeField] int m_size = 5;
#pragma warning restore 0649
        private Vector2Int? m_head;

        void Start()
        {
            Observable.EveryUpdate()
                .Select(ReadTexturePos)
                .Where(texPos => texPos.HasValue)
                .Select(texPos => texPos.Value)
                .Buffer(3)
                .Subscribe(Draw)
                .AddTo(this);
        }

        private Vector2Int? ReadTexturePos(long _)
        {
            var mousePos = Mouse.current.position.ReadValue();
            var texPos = m_surface.WorldToTexturePos(m_camera.ScreenToWorldPoint(mousePos));
            return texPos;
        }

        private void Draw(IList<Vector2Int> texPosList)
        {
            // ignore if not dragged a pointer.
            if(!Mouse.current.leftButton.isPressed)
            {
                m_head = null;
                return;
            }
            // connect a points.
            Vector2Int? ptr = m_head;
            foreach(var texPos in texPosList)
            {
                if(ptr != null && ptr.Value != texPos)
                {
                    DrawLine(ptr.Value, texPos);
                }
                ptr = texPos;
            }
            m_surface.Apply();
            m_head = texPosList.Last();
        }

        private void DrawLine(Vector2Int a, Vector2Int b)
        {
            // bresenham algorithm
            var diff = b - a;
            bool xOrigin = Mathf.Abs(b.x - a.x) > Mathf.Abs(b.y - a.y);
            int move = 0;
            while(true)
            {
                if (xOrigin) {
                    a.x += (int)Mathf.Sign(diff.x);
                    move += Mathf.Abs(diff.y);
                    m_surface.FillRect(a, m_color, m_size);
                    while (move >= Mathf.Abs(diff.x))
                    {
                        move -= Mathf.Abs(diff.x);
                        a.y += (int)Mathf.Sign(diff.y);
                        m_surface.FillRect(a, m_color, m_size);
                    }
                    if(a.x == b.x)
                    {
                        break;
                    }
                }
                else
                {
                    a.y += (int)Mathf.Sign(diff.y);
                    move += Mathf.Abs(diff.x);
                    m_surface.FillRect(a, m_color, m_size);
                    while (move >= Mathf.Abs(diff.y))
                    {
                        move -= Mathf.Abs(diff.y);
                        a.x += (int)Mathf.Sign(diff.x) ;
                        m_surface.FillRect(a, m_color, m_size);
                    }
                    if (a.y == b.y)
                    {
                        break;
                    }
                }
            }
        }
    }
}
````