【Unity】UI上でお絵描きしてみよう!ShaderとRawImageを使った簡単ドロー機能
UnityでUI上に直接お絵かきしたいことってありませんか?
この記事では、RawImage + RenderTexture + Shader を使って、UI上にスムーズな線を描ける機能の作り方をご紹介します!
できること
- UI(Canvas)上でマウス or タッチ操作によるお絵描き
- Shaderでなめらかなブラシ処理
- 補完付きの線描画で滑らかな体験
仕組みの全体像
RawImageに描画用のRenderTextureをセット- シェーダーで描画点に応じて色をブレンド
- マウスやタッチ操作で描画位置を制御
ブラシ描画用の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座標に変換し、マテリアルに座標を渡して描画します。
💡 主な処理内容:
OnBeginDragとOnDragで描画開始Draw()でクリック位置をUV変換Graphics.Blit()を使って RenderTexture に描きこみ- 距離がある場合は
DrawInterpolate()で補完
🖌 ブラシサイズや色も自由に変更できます。
セットアップ方法
実行イメージ
