読者です 読者をやめる 読者になる 読者になる

しゅみぷろ

プログラミングとか

Splatoonの塗りみたいのを再現したい その5

はじめに

f:id:es_program:20160508185049j:plain:w400

塗り表現その5です。今回はとうとう、動的テクスチャペイントについて解説していきます。

f:id:es_program:20160508211649g:plain:w400

サンプルはこちら(© UTJ/UCL)から遊べます

今までの記事では、ちょこちょこと法線マップやエフェクトについて書いてきました。 今回はズバリ、「塗り」そのものについて書きます。結構順不同で適当に書いているわけですが、今回がメインディッシュで今までのは前座です。

ちなみに

といった制約もありますのでご注意ください。

ペイントまでの流れ

まずはペイントまでにどんなフローが存在するのか整理したいと思います。これはSplatoonの塗りみたいのを再現したい その1 - しゅみぷろでも書いていますが、改めて見ていきます。

  1. オブジェクトの塗りたい位置を決める
  2. その位置はテクスチャでいうとどの部分なのか(Texcoord)求める
  3. Texcoordを指定して、その部分にブラシ画像で色を塗れる専用のシェーダーに「塗れ!」と命令する

という感じです。ただし、もともとオブジェクトについているテクスチャに直接書き込むのではなく、最初にテクスチャを複製し、後のシェーディングではそのテクスチャを用いるようにします。

UVの算出については以下も参考にしてください。

esprog.hatenablog.com

はじめに記述した通り、今回はUVの算出はRaycastHitのみで話を進めていきます。

ペイント用コンポーネント

ペイントしたいオブジェクトには

  • MeshCollider
  • DynamicPaintObject

の2つをアタッチします。MeshColliderはUnityが用意しているコンポーネントです。当たり判定を正確に取るために、MeshFilterと同じMeshを設定することを強くお勧めします。というのも、オブジェクト表面上の点を正確に取ってこれないとUVの計算が出来ないためです。

DynamicPaintObjectは以下のスクリプトです。

using System.Collections;
using System.Linq;
using UnityEngine;

namespace Es.TexturePaint
{
  [RequireComponent(typeof(Material))]
  [RequireComponent(typeof(MeshRenderer))]
  [RequireComponent(typeof(MeshCollider))]
  public class DynamicPaintObject : MonoBehaviour
  {
    #region SerializedProperties

    [SerializeField, Tooltip("メインテクスチャのプロパティ名")]
    private string mainTextureName = "_MainTex";

    [SerializeField, Tooltip("バンプマップテクスチャのプロパティ名")]
    private string bumpTextureName = "_BumpMap";

    [SerializeField, Tooltip("テクスチャペイント用マテリアル")]
    private Material paintMaterial = null;

    [SerializeField, Tooltip("ブラシ用テクスチャ")]
    private Texture2D blush = null;

    [SerializeField, Range(0, 1), Tooltip("ブラシの大きさ")]
    private float blushScale = 0.1f;

    [SerializeField, Tooltip("ブラシの色")]
    private Color blushColor = default(Color);

    [SerializeField, Tooltip("ブラシバンプマップ用マテリアル")]
    private Material paintBumpMaterial = null;

    [SerializeField, Tooltip("ブラシバンプマップ用テクスチャ")]
    private Texture2D blushBump = null;

    [SerializeField, Range(0, 1), Tooltip("バンプマップブレンド比率")]
    private float bumpBlend = 1;

    #endregion SerializedProperties

    #region ShaderPropertyID

    private int mainTexturePropertyID;
    private int bumpTexturePropertyID;
    private int paintUVPropertyID;
    private int blushTexturePropertyID;
    private int blushScalePropertyID;
    private int blushColorPropertyID;
    private int blushBumpTexturePropertyID;
    private int blushBumpBlendPropertyID;

    #endregion ShaderPropertyID

    private RenderTexture paintTexture;
    private RenderTexture paintBumpTexture;
    private Material material;

    /// <summary>
    /// ブラシの大きさ
    /// [0,1]の範囲をとるテクスチャサイズの比
    /// </summary>
    public float BlushScale
    {
      get { return Mathf.Clamp01(blushScale); }
      set { blushScale = Mathf.Clamp01(value); }
    }

    public float BumpBlend
    {
      get { return Mathf.Clamp01(bumpBlend); }
      set { bumpBlend = Mathf.Clamp01(value); }
    }

    /// <summary>
    /// ブラシの色
    /// </summary>
    public Color BlushColor
    {
      get { return blushColor; }
      set { blushColor = value; }
    }

    /// <summary>
    /// ブラシのテクスチャ
    /// </summary>
    public Texture BlushTexture
    {
      get { return blush; }
      set { blush = (Texture2D)value; }
    }

    /// <summary>
    /// ブラシのバンプマップテクスチャ
    /// </summary>
    public Texture BlushBumpTexture
    {
      get { return blushBump; }
      set { blushBump = (Texture2D)value; }
    }

    #region UnityEventMethod

    public void Awake()
    {
      InitPropertyID();

      var meshRenderer = GetComponent<MeshRenderer>();
      material = meshRenderer.material;
      var mainTexture = material.GetTexture(mainTexturePropertyID);
      var bumpTexture = material.GetTexture(bumpTexturePropertyID);

      //Textureがもともとついてないオブジェクトには非対応
      if(mainTexture == null)
      {
        Debug.LogWarning("[DynamicPaintObject] : テクスチャの設定されていないオブジェクトに適用することはできません");
        Destroy(this);
        return;
      }
      else
      {
        //DynamicPaint用RenderTextureの生成
        paintTexture = new RenderTexture(mainTexture.width, mainTexture.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default);
        //メインテクスチャのコピー
        Graphics.Blit(mainTexture, paintTexture);
        //マテリアルのテクスチャをRenderTextureに変更
        material.SetTexture(mainTexturePropertyID, paintTexture);//TODO:_MainTexではなく、テクスチャを指定できるように(あと文字列やめたい)
      }

      if(blushBump != null && bumpTexture == null)
      {
        Debug.LogWarning("[DynamicPaintObject] : バンプマップテクスチャの設定されていないオブジェクトに適用することはできません");
        Destroy(this);
        return;
      }
      {
        //TODO:法線マップテクスチャの生成(インスペクターからこの機能を使うか任意にするべき)
        paintBumpTexture = new RenderTexture(mainTexture.width, mainTexture.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default);
        //法線マップのコピー
        Graphics.Blit(bumpTexture, paintBumpTexture);
        //マテリアルの法線マップテクスチャをRenderTextureに変更
        material.SetTexture(bumpTexturePropertyID, paintBumpTexture);
      }
    }

#if UNITY_EDITOR && DEBUG_DYNAMIC_TEXTURE_PAINT
    public void OnGUI()
    {
      GUILayout.Label("Main Shader: " + paintMaterial.shader.name);
      GUILayout.Label("Bump Shader: " + paintBumpMaterial.shader.name);
      GUILayout.Label("Blush Main: " + (blush != null).ToString());
      GUILayout.Label("Blush Bump: " + (blushBump != null).ToString());
      GUILayout.Label("Support Main: " + paintMaterial.shader.isSupported);
      GUILayout.Label("Support Bump: " + paintBumpMaterial.shader.isSupported);
      GUILayout.Label("RenderTexture Main: " + (paintTexture != null).ToString());
      GUILayout.Label("RenderTexture Bump: " + (paintBumpTexture != null).ToString());
      GUILayout.Label("Main Texture ID:" + mainTexturePropertyID);
      GUILayout.Label("Bump Texture ID:" + bumpTexturePropertyID);
      GUILayout.Label("Paint UV ID:" + paintUVPropertyID);
      GUILayout.Label("Blush Main Texture ID:" + blushTexturePropertyID);
      GUILayout.Label("Blush Bump Texture ID:" + blushBumpTexturePropertyID);
      GUILayout.Label("Blush Scale ID:" + blushScalePropertyID);
      GUILayout.Label("Blush Color ID:" + blushColorPropertyID);
      GUILayout.Label("Blush Bump Blend ID::" + blushBumpBlendPropertyID);
    }
#endif

    #endregion UnityEventMethod

    /// <summary>
    /// シェーダーのプロパティIDを初期化する
    /// </summary>
    private void InitPropertyID()
    {
      mainTexturePropertyID = Shader.PropertyToID(mainTextureName);
      bumpTexturePropertyID = Shader.PropertyToID(bumpTextureName);

      paintUVPropertyID = Shader.PropertyToID("_PaintUV");
      blushTexturePropertyID = Shader.PropertyToID("_Blush");
      blushScalePropertyID = Shader.PropertyToID("_BlushScale");
      blushColorPropertyID = Shader.PropertyToID("_BlushColor");
      blushBumpTexturePropertyID = Shader.PropertyToID("_BlushBump");
      blushBumpBlendPropertyID = Shader.PropertyToID("_BumpBlend");
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blush">ブラシテクスチャ</param>
    /// <param name="blushColor">ブラシカラー</param>
    /// <param name="blushScale">ブラシの大きさ(UV座病スケール値[0,1])</param>
    /// <param name="blushBump">ブラシのバンプマップテクスチャ</param>
    /// <param name="bumpBlend">バンプマップテクスチャブレンド(ブレンド率[0,1])</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, Texture blush, Color blushColor, float blushScale, Texture blushBump, float bumpBlend)
    {
      if(hitInfo.collider != null && hitInfo.collider.gameObject == gameObject)
      {
        var uv = hitInfo.textureCoord;
        RenderTexture buf = RenderTexture.GetTemporary(paintTexture.width, paintTexture.height);

        #region ErrorCheck

        if(buf == null)
        {
          Debug.LogError("テンポラリテクスチャの生成に失敗しました。");
          return false;
        }
        if(blush == null)
        {
          Debug.LogError("ブラシが設定されていません。値がNULLです。");
          return false;
        }
        if(paintMaterial == null)
        {
          Debug.LogError("ブラシ用のマテリアルが設定されていません。値がNULLです。");
          return false;
        }

        #endregion ErrorCheck

        #region ParameterDeform

        blushScale = Mathf.Clamp01(blushScale);
        bumpBlend = Mathf.Clamp01(bumpBlend);

        #endregion ParameterDeform

        //メインテクスチャへのペイント
        paintMaterial.SetVector(paintUVPropertyID, uv);
        paintMaterial.SetTexture(blushTexturePropertyID, blush);
        paintMaterial.SetFloat(blushScalePropertyID, blushScale);
        paintMaterial.SetVector(blushColorPropertyID, blushColor);
        Graphics.Blit(paintTexture, buf, paintMaterial);
        Graphics.Blit(buf, paintTexture);

        //バンプマップへのペイント

        #region LogWarningBumpMap

        //オブジェクト生成時にBumpMapテクスチャが設定されていなかったが、メソッドにブラシバンプマップテクスチャが渡された
        if(blushBump != null && paintBumpTexture == null)
        {
          Debug.LogWarning("初期化時にバンプマップテクスチャを生成していません。バンプマップ処理をスキップします。");
        }

        #endregion LogWarningBumpMap

        if(blushBump != null && paintBumpTexture != null)
        {
          paintBumpMaterial.SetVector(paintUVPropertyID, uv);
          paintBumpMaterial.SetTexture(blushTexturePropertyID, blush);
          paintBumpMaterial.SetTexture(blushBumpTexturePropertyID, blushBump);
          paintBumpMaterial.SetFloat(blushScalePropertyID, blushScale);
          paintBumpMaterial.SetFloat(blushBumpBlendPropertyID, bumpBlend);
          Graphics.Blit(paintBumpTexture, buf, paintBumpMaterial);
          Graphics.Blit(buf, paintBumpTexture);
        }

        RenderTexture.ReleaseTemporary(buf);
        return true;
      }
      return false;
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blush">ブラシテクスチャ</param>
    /// <param name="blushColor">ブラシカラー</param>
    /// <param name="blushBump">ブラシのバンプマップテクスチャ</param>
    /// <param name="bumpBlend">バンプマップテクスチャブレンド(ブレンド率[0,1])</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, Texture blush, Color blushColor, Texture blushBump, float bumpBlend)
    {
      return Paint(hitInfo, blush, blushColor, BlushScale, blushBump, bumpBlend);
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blush">ブラシテクスチャ</param>
    /// <param name="blushBump">ブラシのバンプマップテクスチャ</param>
    /// <param name="bumpBlend">バンプマップテクスチャブレンド(ブレンド率[0,1])</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, Texture blush, Texture blushBump, float bumpBlend)
    {
      return Paint(hitInfo, blush, BlushColor, BlushScale, blushBump, bumpBlend);
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blush">ブラシテクスチャ</param>
    /// <param name="blushBump">ブラシのバンプマップテクスチャ</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, Texture blush, Texture blushBump)
    {
      return Paint(hitInfo, blush, BlushColor, BlushScale, blushBump, BumpBlend);
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blushBump">ブラシのバンプマップテクスチャ</param>
    /// <param name="bumpBlend">バンプマップテクスチャブレンド(ブレンド率[0,1])</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, Texture blushBump, float bumpBlend)
    {
      return Paint(hitInfo, BlushTexture, BlushColor, BlushScale, blushBump, bumpBlend);
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blush">ブラシテクスチャ</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, Texture blush)
    {
      return Paint(hitInfo, blush, BlushColor, BlushScale, BlushBumpTexture, BumpBlend);
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blushColor">ブラシカラー</param>
    /// <param name="blushScale">ブラシの大きさ(UV座病スケール値[0,1])</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, Color blushColor, float blushScale)
    {
      return Paint(hitInfo, BlushTexture, blushColor, blushScale, BlushBumpTexture, BumpBlend);
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blushColor">ブラシカラー</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, Color blushColor)
    {
      return Paint(hitInfo, BlushTexture, blushColor, BlushScale, BlushBumpTexture, BumpBlend);
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blushScale">ブラシの大きさ(UV座病スケール値[0,1])</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, float blushScale)
    {
      return Paint(hitInfo, BlushTexture, BlushColor, blushScale, BlushBumpTexture, BumpBlend);
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blush">ブラシテクスチャ</param>
    /// <param name="blushColor">ブラシカラー</param>
    /// <param name="blushScale">ブラシの大きさ(UV座病スケール値[0,1])</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, Texture blush, Color blushColor, float blushScale)
    {
      return Paint(hitInfo, blush, blushColor, blushScale, BlushBumpTexture, BumpBlend);
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <param name="blush">ブラシテクスチャ</param>
    /// <param name="blushColor">ブラシカラー</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo, Texture blush, Color blushColor)
    {
      return Paint(hitInfo, blush, blushColor, BlushScale, BlushBumpTexture, BumpBlend);
    }

    /// <summary>
    /// ペイント処理
    /// </summary>
    /// <param name="hitInfo">RaycastのHit情報</param>
    /// <returns>ペイントの成否</returns>
    public bool Paint(RaycastHit hitInfo)
    {
      return Paint(hitInfo, BlushTexture, BlushColor, BlushScale, BlushBumpTexture, BumpBlend);
    }
  }
}

まだ不完全な部分もありますが、概ねこんな感じになっています。注目すべきはPaintメソッドです。この部分が『Texcoordを指定して、その部分にブラシ画像で色を塗れる専用のシェーダーに「塗れ!」と命令する』部分です。シェーダーに必要な値を渡してBlitしてるだけですので全く難しくないと思います。

塗りシェーダー

さて、「塗れ!」と命令されたシェーダーがどのように塗りを実現していくのかについて見ていきます。

Shader "Es/TexturePaint/Paint"{
    Properties{
        [HideInInspector]
        _MainTex("MainTex", 2D) = "white"
        [HideInInspector]
        _Blush("Blush", 2D) = "white"
        [HideInInspector]
        _BlushScale("BlushScale", FLOAT) = 0.1
        [HideInInspector]
        _BlushColor("BlushColor", VECTOR) = (0,0,0,0)
        [HideInInspector]
        _PaintUV("Hit UV Position", VECTOR) = (0,0,0,0)
    }

    SubShader{

        CGINCLUDE

            struct app_data {
                float4 vertex:POSITION;
                float4 uv:TEXCOORD0;
            };

            struct v2f {
                float4 screen:SV_POSITION;
                float4 uv:TEXCOORD0;
            };

            sampler2D _MainTex;
            sampler2D _Blush;
            float4 _PaintUV;
            float _BlushScale;
            float4 _BlushColor;
        ENDCG

        Pass{
            CGPROGRAM
#pragma vertex vert
#pragma fragment frag

            v2f vert(app_data i) {
                v2f o;
                o.screen = mul(UNITY_MATRIX_MVP, i.vertex);
                o.uv = i.uv;
                return o;
            }


            float4 frag(v2f i) : SV_TARGET {
                float h = _BlushScale;
                if (_PaintUV.x - h < i.uv.x && i.uv.x < _PaintUV.x + h &&
                        _PaintUV.y - h < i.uv.y && i.uv.y < _PaintUV.y + h) {
                    float4 col = tex2D(_Blush, (_PaintUV.xy - i.uv) / h * 0.5 + 0.5);
                    if (col.a - 1 >= 0)
                        return _BlushColor;
                }

                return tex2D(_MainTex, i.uv);
            }

            ENDCG
        }
    }
}

概ねこんな感じです。結構無理矢理ですが、まぁいいかなと。。

(_PaintUV.xy - i.uv) / h * 0.5 + 0.5

で、テクスチャに塗るブラシのサイズと位置を計算しています。ちなみにこちらで紹介した法線マップを適用するシェーダーも基本的には同じですが、ブラシ用のテクスチャを参照して必要ない部分への法線マップの書き込みを制御しています。

Shader "Es/TexturePaint/PaintBump"{
    Properties{
        [HideInInspector]
        _MainTex("MainTex", 2D) = "white"
        [HideInInspector]
        _Blush("Blush", 2D) = "white"
        [HideInInspector]
        _BlushBump("BlushBump", 2D) = "white"
        [HideInInspector]
        _BlushScale("BlushScale", FLOAT) = 0.1
        [HideInInspector]
        _PaintUV("Hit UV Position", VECTOR) = (0,0,0,0)
        [HideInInspector]
        _BumpBlend("BumpBlend", FLOAT) = 1
    }

    SubShader{

        CGINCLUDE

            struct app_data {
                float4 vertex:POSITION;
                float4 uv:TEXCOORD0;
            };

            struct v2f {
                float4 screen:SV_POSITION;
                float4 uv:TEXCOORD0;
            };

            sampler2D _MainTex;
            sampler2D _Blush;
            sampler2D _BlushBump;
            float4 _PaintUV;
            float _BlushScale;
            float _BumpBlend;
        ENDCG

        Pass{
            CGPROGRAM
#pragma vertex vert
#pragma fragment frag

            v2f vert(app_data i) {
                v2f o;
                o.screen = mul(UNITY_MATRIX_MVP, i.vertex);
                o.uv = i.uv;
                return o;
            }


            float4 frag(v2f i) : SV_TARGET {
                float h = _BlushScale;

            float4 base = tex2D(_MainTex, i.uv);

                if (_PaintUV.x - h < i.uv.x && i.uv.x < _PaintUV.x + h &&
                        _PaintUV.y - h < i.uv.y && i.uv.y < _PaintUV.y + h) {
                            float4 brushCol = tex2D(_Blush, (_PaintUV.xy - i.uv) / h * 0.5 + 0.5);
                            if (brushCol.a - 1 >= 0) {
                                float4 bump = tex2D(_BlushBump, (_PaintUV.xy - i.uv) / h * 0.5 + 0.5);
                                return normalize(lerp(base, bump, _BumpBlend));
                            }
                }

                return base;
            }

            ENDCG
        }
    }
}

実際に使う

近々、もうちょっとコード整理してUnityPackage形式で配布予定です。 それまでしばしお待ち下さい。。

一応どんな感じで使えるのかだけ先に書いておきます。

f:id:es_program:20160501142352g:plain:w400

実際に使うために必要な手順は

  1. ペイント用コンポーネント(DynamicPaintObject)をオブジェクトに取り付けてパラメーターを好きに弄る
  2. スクリプトから塗りを命令する

だけです。

Unityを使ったことがある方なら、1は簡単だと思います。 2については、サンプルのコードを見てもらう方がはやいでしょう。

using Es.TexturePaint;
using System.Collections;
using UnityEngine;

namespace Es.TexturePaint.Sample
{
  public class MousePainter : MonoBehaviour
  {
    private void Update()
    {
      if(Input.GetMouseButtonDown(0))
      {
        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hitInfo;
        if(Physics.Raycast(ray, out hitInfo))
        {
          var paintObject = hitInfo.transform.GetComponent<DynamicPaintObject>();
          if(paintObject != null)
            paintObject.Paint(hitInfo);
        }
      }
    }
  }
}

DynamicPaintObjectを取得して、Paintメソッドを呼び出す。これだけです。 上記のサンプルではマウスクリック位置にレイを飛ばし、ヒットしたらDynamicPaintObjectを取得、Paint命令を出します。オーバーロードされている命令を使うことで、ブラシや色、サイズを変更したり出来るようになっています。