アヒルのある日

株式会社AHIRUの社員ブログです。毎週更新!社員が自由に思いついたことを書きます。

楽しい塗り絵の時間!(#2)

こんにちは。ちゃらいプログラマです。
前回はシェーダー側の解説を行いましたので今回はC#側の解説を行います。

f:id:charai_ahiru:20210517122516p:plain

C#側での対応は以下になります。
1.画面タッチした座標からピクセルカラーを取得
2.シェーダーに1.で取得した色(InColor)と塗りたい色(OutColor)を渡す
3.バッファテクスチャに更新分を書き込み反映するテクスチャをスプライトに変換する

※2.に関しては処理が分散しているため全体フローで解説します ※

1.画面タッチした座標からピクセルカラーを取得

画面タッチした座標(スクリーン座標)を基準に計算してきます。

Texture2D _baseTexture = new Texture2D( 1, 1, TextureFormat.RGBA32, false );

// _paintPositionにはタッチした座標が入る
// _paintPosition = PointerEventData.position;
float x =  Mathf.Clamp( _paintPosition.x, 0f, Screen.width - 1f );
float y = Mathf.Clamp( _paintPosition.y, 0f, Screen.height - 1f );

_baseTexture.ReadPixels( new Rect( x, y, 1f, 1f ), 0, 0 );

3.バッファテクスチャに更新分を書き込み反映する

マテリアルに登録されているメインテクスチャを元にレンダーテクスチャを生成し更新していきます。

RenderTexture        _paintTexture           = null;
RenderTexture       _bufferTexture          = null;

// ペイントテクスチャ生成
_paintTexture = new RenderTexture( _image.material.mainTexture.width, _image.material.mainTexture.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default );
Graphics.Blit( _image.material.mainTexture, _paintTexture );

// 更新分を書き込み反映

// バッファ生成
_bufferTexture = RenderTexture.GetTemporary( _paintTexture.width, _paintTexture.height );

// バッファからペイントテクスチャへコピー
Graphics.Blit( _paintTexture, _bufferTexture, _image.material );
Graphics.Blit( _bufferTexture, _paintTexture );

// バッファ解放
RenderTexture.ReleaseTemporary( _bufferTexture );

全体フロー

今回は現在の色を元にどの部分を塗れるかを判断しているため 「フレーム完了イベント」にて1.のタッチ座標のピクセルカラー取得を行っています。(_onEndOfFrame関数)

public class Paint : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    //-----------------------------------------------------------------
    // 変数定義
    //-----------------------------------------------------------------
    [SerializeField]
    private Image               _image                  = null;
    [SerializeField]
    private Text                _toggleText             = null;
    [SerializeField]
    private Slider              _sliderBrushRadius      = null;

    private Texture2D           _baseTexture            = null;
    private RenderTexture       _paintTexture           = null;
    private RenderTexture       _bufferTexture          = null;
    private Vector2             _paintPosition          = Vector2.zero;
    private Color               _paintColor             = Color.white;
    private Vector4             _brush                  = Vector4.zero;
    private bool                _isBrush                = false;



    //-----------------------------------------------------------------
    // システム関数定義
    //-----------------------------------------------------------------
    // 初期化前処理
    private void Awake()
    {
        // ベーステクスチャ生成
        _baseTexture = new Texture2D( 1, 1, TextureFormat.RGBA32, false );

        // ペイントテクスチャ生成
        _paintTexture = new RenderTexture( _image.material.mainTexture.width, _image.material.mainTexture.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default );
        Graphics.Blit( _image.material.mainTexture, _paintTexture );

        // シェーダー初期化
        _image.material.mainTexture = _paintTexture;
        _image.material.SetColor( "_InColor", Color.white );
        _image.material.SetColor( "_OutColor", Color.white );
        _image.material.SetVector( "_Brush", _brush );
        _image.material.SetFloat( "_BrushRadius", 0f );

        _toggleText.text = "ブラシ";

        // フレーム終了イベントコルーチン起動
        StartCoroutine( _onEndOfFrame() );
    }

    // 破棄処理
    private void OnDestroy()
    {
        _paintTexture.Release();

        Destroy( _paintTexture );
        Destroy( _bufferTexture );
        Destroy( _baseTexture );
    }

    // クリックイベント
    public void OnPointerClick( PointerEventData eventData )
    {
        if( _isBrush )
        {
            return;
        }

        _paintPosition = eventData.position;

        // 塗りつぶし設定
        _setPaintFill();
    }

    // ドラッグ開始イベント
    public void OnBeginDrag( PointerEventData eventData )
    {
        if( !_isBrush )
        {
            return;
        }

        _paintPosition = eventData.position;
    }

    // ドラッグ中イベント
    public void OnDrag( PointerEventData eventData )
    {
        if( !_isBrush )
        {
            return;
        }
        if( _paintPosition != Vector2.zero )
        {
            return;
        }

        // ブラシ設定
        _setPaintBrush( eventData.position );
        // スプライトコピー
        _copySprite();
    }

    // ドラッグ終了イベント
    public void OnEndDrag( PointerEventData eventData )
    {
        _image.material.SetFloat( "_BrushRadius", 0f );
    }



    //-----------------------------------------------------------------
    // 関数定義
    //-----------------------------------------------------------------
    /// <summary>
    /// 選択カラーボタンクリックイベント
    /// </summary>
    /// <param name="image">画像</param>
    public void OnClickSelectColor( Image image )
    {
        _paintColor = image.color;
    }

    /// <summary>
    /// トグルボタンクリックイベント
    /// </summary>
    public void OnClickToggleBrush()
    {
        _isBrush = !_isBrush;

        _toggleText.text = _isBrush ? "塗りつぶし" : "ブラシ";
    }



    /// <summary>
    /// 塗りつぶし設定
    /// </summary>
    private void _setPaintFill()
    {
        _brush = Vector4.zero;
        _image.material.SetVector( "_Brush", _brush );
    }

    /// <summary>
    /// ブラシ設定
    /// </summary>
    /// <param name="position">ブラシ座標</param>
    private void _setPaintBrush( Vector2 position )
    {
        Vector3[] cornerList = new Vector3[4];
        _image.rectTransform.GetWorldCorners( cornerList );

        Vector2 corner0 = RectTransformUtility.WorldToScreenPoint( Camera.main, cornerList[0] );
        Vector2 corner2 = RectTransformUtility.WorldToScreenPoint( Camera.main, cornerList[2] );

        _brush.x = (position.x - corner0.x) / (corner2.x - corner0.x);
        _brush.y = (position.y - corner0.y) / (corner2.y - corner0.y);
        _brush.z = 1f;
        
        _image.material.SetVector( "_Brush", _brush );
        _image.material.SetFloat( "_BrushRadius",_sliderBrushRadius.value );
    }

    /// <summary>
    /// スプライトコピー
    /// </summary>
    private void _copySprite()
    {
        // バッファ生成
        _bufferTexture = RenderTexture.GetTemporary( _paintTexture.width, _paintTexture.height );

        // バッファからペイントテクスチャへコピー
        Graphics.Blit( _paintTexture, _bufferTexture, _image.material );
        Graphics.Blit( _bufferTexture, _paintTexture );

        // バッファ解放
        RenderTexture.ReleaseTemporary( _bufferTexture );
    }

    /// <summary>
    /// フレーム終了イベント
    /// </summary>
    /// <returns></returns>
    private IEnumerator _onEndOfFrame()
    {
        var waitForEndOfFrame = new WaitForEndOfFrame();
        
        while( true )
        {
            yield return waitForEndOfFrame;

            if( _paintPosition != Vector2.zero )
            {
                float x = Mathf.Clamp( _paintPosition.x, 0f, Screen.width - 1f );
                float y = Mathf.Clamp( _paintPosition.y, 0f, Screen.height - 1f );

                _baseTexture.ReadPixels( new Rect( x, y, 1f, 1f ), 0, 0 );

                _image.material.SetColor( "_InColor", _baseTexture.GetPixel( 0, 0 ) );
                _image.material.SetColor( "_OutColor", _paintColor );

                // ドラッグ開始用ブラシ設定
                if( _isBrush )
                {
                    _setPaintBrush( _paintPosition );
                }
                // スプライトコピー
                _copySprite();

                _paintPosition = Vector2.zero;
            }
        }
    }
} 

Inspector設定は以下のようになります。 f:id:charai_ahiru:20210517122915p:plain

基礎はできたのであとは、いい感じに改良を行ってリリースまで持っていきたいところです。
ではまた次回!