うにてぃブログ

UnityやUnreal Engineの記事を書いていきます

【Unity】UI上でお絵描きしてみよう!ShaderとRawImageを使った簡単ドロー機能

UnityでUI上に直接お絵かきしたいことってありませんか?
この記事では、RawImage + RenderTexture + Shader を使って、UI上にスムーズな線を描ける機能の作り方をご紹介します!

できること

  • UI(Canvas)上でマウス or タッチ操作によるお絵描き
  • Shaderでなめらかなブラシ処理
  • 補完付きの線描画で滑らかな体験

仕組みの全体像

  1. RawImage に描画用の RenderTexture をセット
  2. シェーダーで描画点に応じて色をブレンド
  3. マウスやタッチ操作で描画位置を制御

ブラシ描画用のShader

このシェーダーは、クリック/タッチされた位置に円形のブラシを描きこむものです。

🔒 Shader "Hidden/Draw"
この名前で保存しておくと、インスペクタに表示されず扱いやすくなります。

Shader "Hidden/Draw"
{
    Properties
    {
        _SourceTex ("Texture", 2D) = "white" {}
        _Coordinate ("Coordinate", Vector) = (0, 0, 0, 0)
        _Color ("Color", Color) = (1, 1, 1, 1)
        _TextureSize ("Size", Vector) = (0, 0, 0, 0)
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _SourceTex;
            float4 _Coordinate; // (x, y, radius, threshold)
            float4 _Color;
            float2 _TextureSize;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                float aspect = _TextureSize.x / _TextureSize.y;
                float2 texelPos = i.uv;
                float2 drawPos = _Coordinate.xy;

                float2 diff = texelPos - drawPos;
                diff.x *= aspect;
                float distance = length(diff);

                float mask = smoothstep(_Coordinate.z, _Coordinate.z * 0.8, distance);
                half4 color = tex2D(_SourceTex, i.uv);
                half4 destCol = lerp(color, _Color, mask);
                return destCol;
            }

            ENDCG
        }
    }
}

スクリプト

このスクリプトでは、マウスのドラッグイベントを使って描画を制御します。

using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class DrawCanvas : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler
{
    private static readonly int MainTex = Shader.PropertyToID("_SourceTex");
    private static readonly int Coordinate = Shader.PropertyToID("_Coordinate");
    private static readonly int TextureSize = Shader.PropertyToID("_TextureSize");
    private static readonly int Color = Shader.PropertyToID("_Color");

    [SerializeField]
    private Shader _drawShader;

    [SerializeField]
    private RawImage _rawImage;
    
    [SerializeField]
    private float _radius = 0.1f;

    [SerializeField]
    private Color _color = UnityEngine.Color.black;

    private RenderTexture _texture;
    private Material _drawMaterial;
    
    private Vector2 _screenPointMax;
    private Vector2 _screenPointMin;
    
    private Vector2 _lastPosition;

    private void Start()
    {
        var rectTransform = transform as RectTransform;
        _texture = new RenderTexture(
            (int)rectTransform.rect.width,
            (int)rectTransform.rect.height,
            0,
            RenderTextureFormat.ARGB32)
        {
            filterMode = FilterMode.Bilinear,
            wrapMode = TextureWrapMode.Clamp,
            enableRandomWrite = true
        };
        _texture.Create();

        _drawMaterial = new Material(_drawShader);
        _drawMaterial.SetTexture(MainTex, _texture);
        _drawMaterial.SetVector(TextureSize, new Vector4(_texture.width, _texture.height, 0, 0));
        _drawMaterial.SetColor(Color, _color);
        
        var canvas = transform.GetComponentInParent<Canvas>().rootCanvas;

        var corners = new Vector3[4];
        rectTransform.GetWorldCorners(corners);
     
        var screenPoints = new Vector3[4];
        for (var i = 0; i < corners.Length; i++)
        {
            var screenPoint = UnityEngine.RectTransformUtility.WorldToScreenPoint(canvas.worldCamera, corners[i]);
            screenPoints[i] = screenPoint;
        }

        _screenPointMax = new Vector2(
            screenPoints.Max(v => v.x),
            screenPoints.Max(v => v.y)
        );
     
        _screenPointMin = new Vector2(
            screenPoints.Min(v => v.x),
            screenPoints.Min(v => v.y)
        );

        _rawImage.texture = _texture;
    }
    
    private void OnDestroy()
    {
        _rawImage.texture = null;
        if (_texture != null)
        {
            _texture.Release();
            Destroy(_texture);
            _texture = null;
        }

        if (_drawMaterial != null)
        {
            Destroy(_drawMaterial);
            _drawMaterial = null;
        }
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        Draw(eventData.position);
        _lastPosition = eventData.position;
    }
    
    public void OnDrag(PointerEventData eventData)
    {
        DrawInterpolate(eventData.position);
        _lastPosition = eventData.position;
    }


    public void OnEndDrag(PointerEventData eventData)
    {
    }

    private void DrawInterpolate(Vector2 position)
    {
        Draw(position);
        var delta = position - _lastPosition;
        var distance = delta.magnitude;
        const float step = 10f;
        // 一定以上離れていたら補完
        if (distance > step)
        {
            var count = (int)(distance / step);
            for (var i = 0; i < count; i++)
            {
                var t = (float)i / count;
                var interpolatePosition = Vector2.Lerp(_lastPosition, position, t);
                Draw(interpolatePosition);
            }
        }
    }

    private void Draw(Vector2 screenPosition)
    {
        var u = Mathf.InverseLerp(_screenPointMin.x, _screenPointMax.x, screenPosition.x);
        var v = Mathf.InverseLerp(_screenPointMin.y, _screenPointMax.y, screenPosition.y);
        
        _drawMaterial.SetVector(Coordinate, new Vector4(u, v, _radius, 0));
        var temp = RenderTexture.GetTemporary(_texture.width, _texture.height, 0, _texture.format);
        Graphics.Blit(_texture, temp, _drawMaterial);
        Graphics.Blit(temp, _texture);
        RenderTexture.ReleaseTemporary(temp);
    }

    public void Clear()
    {
        RenderTexture currentRT = RenderTexture.active;
        RenderTexture.active = _texture;
        GL.Clear(true, true, UnityEngine.Color.clear);
        RenderTexture.active = currentRT;
    }
}

描画用スクリプトのポイント

描画の制御は、IDragHandler を実装したコンポーネントで行います。
描きたい位置をUV座標に変換し、マテリアルに座標を渡して描画します。

💡 主な処理内容:

  • OnBeginDragOnDrag で描画開始
  • Draw() でクリック位置をUV変換
  • Graphics.Blit() を使って RenderTexture に描きこみ
  • 距離がある場合は DrawInterpolate() で補完

🖌 ブラシサイズや色も自由に変更できます。

セットアップ方法

  1. Canvas上に RawImage を配置
  2. 描画スクリプトを RawImage にアタッチ
  3. インスペクターで Shader と色、サイズを指定
  4. 実行するだけでお絵描き可能!

実行イメージ