Synamon’s Engineer blog

Synamonでは、VR空間に複数人が同時に接続可能で、多彩な標準機能を搭載している『NEUTRANS』という独自のVRシステムを開発しています。ビジネスなどの現場でも使いやすいよう、独自の機能や技術を日々追究しています。このブログでは、『NEUTRANS』開発の裏側にあるVR技術と、それを支えるUnityやC#といった技術の話を書いていきます。

メッシュ情報を焼いたテクスチャを使ってTexturePaintする

こんにちは。株式会社Synamonでエンジニアとして『NEUTRANS』の開発をしている岡村(@Sokuhatiku)です。今回は、過去のイベント参加時に作った、立体的なメッシュに対して自然なテクスチャペイントを行う手法を紹介します。

Unity内で立体的なメッシュ(キャラクターなど)に対して動的にペイントする時、一般的には描画したい点のメッシュ情報を取得し、UV座標を使ってテクスチャに書き込むという方法が取られると思います。

その際に

  1. UVが重複していることにより他の部分にも同時に描画されてしまう
  2. UVの切れ目でペイントが途切れてしまう
  3. UVが伸びてペイントが歪んでしまう

といった、UVに起因するいくつかの問題が出てきます。

1に関しては、重複が起こらないようUV展開をすることで、手間をかければ解決することが可能です。しかし、立体物に対してペイントする場合、どうしてもメッシュの何処かにUVの切れ目や歪みが現れてしまい、2、3の問題が出てきてしまいます。

去年(2017年)開催された「例のカノジョ ハッカソン」というイベントに参加した際、例のカノジョや例の部屋をインク的なもので塗りたくる必要があったのですが、扇風機のような複雑なオブジェクトに対してペイントをしようとすると、UVの問題が顕著に出てしまいました。

PositionMap

この問題の解決法として、ペイント時にUV座標ではなく、メッシュ座標を参照して描画する手法を試しました。

テクスチャペイント時には、描画ターゲットがテクスチャである以上、そのままではメッシュの情報を持ってくることが出来ません*1。 その為、事前に特殊なシェーダーで対象テクスチャが貼ってあるメッシュをレンダリングします。

VertexShaderの出力(頂点座標)にUVを、FragmentShaderの出力(Color)にメッシュ座標を与えることで、UV座標をメッシュ座標に変換する2次元マップが作成出来ます

// Graphics.DrawMeshNow()で呼び出すシェーダ
v2f vert (appdata v)
{
    v2f o;
    
    // クリップ空間座標に変換
    float2 uv = v.uv * 2 - 1;

    // 座標系の違いを吸収
    #if UNITY_UV_STARTS_AT_TOP
    uv.y *= -1;
    #endif

    o.vertex = float4(uv, 1, 1); // POSITION
    o.pos = v.vertex; // TEXCOORD0
    return o;
}

float4 frag (v2f i) : SV_Target
{
    return float4(i.pos, 1); // a==1ならばこのピクセルは有効
}

そうやって作ったマップをRenderTexutreに書き込み、テクスチャペイントシェーダーで参照します。

すると、テクスチャに対するペイント時にメッシュ座標を参照出来るため、UVの切れ目や歪みを無視したペイントが可能になります

// Graphics.Blit()で呼び出すシェーダ
fixed4 frag (v2f i) : SV_Target
{
    float4 pos_a = tex2D(_PositionMap, i.uv);
    fixed4 inColor = tex2D(_MainTex, i.uv);

    // ※メッシュのLocalToWorldMatrixは来ていないので、自分で与える必要がある。
    float3 wpos = mul(_ObjectToWorld, float4(pos_a.xyz, 1)).xyz;

    bool dopaint = distance(wpos, _Pos_Rad.xyz) < _Pos_Rad.w;

    fixed4 col = dopaint ? lerp(inColor, _Color, pos_a.a) : inColor;
    return col;
}

実演

恐ろしいUV展開をしたメッシュを用意します。

f:id:Sokuhatiku:20180706000736p:plain:w300
恐ろしいメッシュ

歪んだテクスチャを用意します。

f:id:Sokuhatiku:20180706000947p:plain:w300
縦横比が1:1でないテクスチャ

f:id:Sokuhatiku:20180706165232p:plain:w300
Unity上での様子

塗ります。

f:id:Sokuhatiku:20180706170406g:plain
ペイントの様子

拡大してみると、ポリゴンのエッジでUVが切れているのが分かります。

f:id:Sokuhatiku:20180706170746p:plain:w300
拡大図

単純に座標をマッピングするだけではポリゴンの境界に隙間が出来てしまうので、マップを1px拡張してあります。*2

ソース

プロジェクトデータをGitHubに上げてあります。 github.com

デメリット

通常のTexturePaintに比べて負荷は高いです。今回のサンプルは事前にローカル座標をテクスチャに焼いて使いまわしていますが、SkinnedMeshRendererを搭載したキャラモデル等、メッシュが動くオブジェクトにペイントしようと思った場合、毎フレームマップを作り直す必要が出てきます。

マップの情報量が増えるに従い、テクスチャ容量も問題になってきます。

「例のカノジョ ハッカソン」では、何も考えずマップを増やしまくった結果、GPUにテクスチャが乗り切らず、ペイントする度にテクスチャの転送が発生し、その度に数秒フリーズするという自体に陥りました。テクスチャ圧縮設定の見直しや、サイズの削減でなんとか動くようにはなりましたが…。

その他

描画前に取得するテクスチャを増やしたり、計算方法を変えたりすることで、求める表現に合わせた拡張が出来ます。

例えばテクスチャを射影してデカール的な表現を作ったり、その際にメッシュのノーマルマップを用意することで、裏側まで描画されるのを防いだりといった事が出来ます。

もしくは、射影視点からのZバッファを作成しておく事で、よりリッチな遮蔽表現が可能になるかと思います。

凝れば凝るほどリソースの消費量の上がり方が尋常ではないですが、凝らなくても複雑な3Dモデルに破綻なくペイント出来るという点が魅力的な手法だと思います。

*1:Graphics.Blit()やComputeShader内ではテクスチャが貼ってあるメッシュの情報までは参照できない

*2:今回のケースでは、拡張してもなお、角に塗り残しが発生しました。テクスチャに余裕があれば2px拡張するか、まともなUV展開したメッシュと歪みを最小限に抑えたテクスチャを使うようにしましょう。