質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.92%

[Unity]UV共有してる面にもUV2を使って一か所だけにペイントしたい

解決済

回答 5

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 5,204

score 91

やりたいこと

動的に3Dモデルのテクスチャに書き込むペイントアプリを作成中です。対象のモデルはユニティちゃんのようなSkinnedMeshRendererやMeshRenderer(MeshFilter)をいくつかもっているもので、マウスをクリックしそこからレイをとばしモデルに書き込むようなアプリを考えています。

どのようにして書き込むかわからなかったのですが、こちらで質問した結果、ありがたいことに丁寧に教えてくださった方がいたので書き込むまでは実装することができました。
[Unity]Graphics.Blitで投影テクスチャマッピングシェーダーをテクスチャに反映できない
[Unity]深度バッファを使って表面のみに投影テクスチャマッピング
(コードや実装方法などリンク先に書いてあります。)

対象のテクスチャをレンダーテクスチャに置き換え、投影テクスチャマッピングをサブカメラから行い、CommandBuffer.DrawRendererを使い直接書き込むような仕組みとなっています。これだとモデルの裏側にも書き込んでしまうので深度情報を使いサブカメラから見えるエリアにのみ塗れるようにしています。

問題点

さて、ここまででなんとかペイントはできるようになったのですが、最初の質問の回答者様のおっしゃる通り、UVが重なっている場合(テクスチャの同じ領域を複数の三角形で使っている場合)、うまく塗ることができません。塗りたくないところも塗られてしまいます。そこで、これをどうにか直したいです。

そもそもモデル作成の時点でUVが重ならないようにすればいい話ではありますが、できれば既存のいろんなモデルに対してもこのアプリを使いたいと思っているので、アプリ側でなんとかしたいです。

自分でやったみたことと、何ができなかったか

以下のサイトを参考に、モデルのGenerate Lightmap UVsをオンにしシェーダーではUV2を使用することでうまくいかないか試してみました。
 モデルの頂点をUV展開した先の位置に任意の絵をテクスチャに描き込む

結果思うようにいかなかったです。全頂点に1対1対応するUVの位置がUV2になるとのことでしたが、どうしても変な位置にかきこみされてしまいます。

まず今回の実装ですが、上の質問のリンクにあるものとは少し変えていて、モデルにはデフォルトのUnlitシェーダーを、そしてモデルとは別の空のGameObjectに深度用のセカンドカメラとペイント用のスクリプト(インスペクタからペイント用のマテリアルを設定)をつけ、モデルをマウスクリックするたびに、モデルのほうへそのカメラを向け、深度カメラのレンダリングをしつつペイント用のマテリアルにプロジェクション行列やペイントに必要な値を渡しつつコマンドバッファで直接塗りを行っています。(長くなるので今回はC#のソースは書きませんが、もしこれだけでわかりづらい場合は言っていただければ公開します。)

シェーダーコードは一番下に置いておきますが、前回の質問時に得られたものからあまりかわりません。重要な部分はuvをuv2に変えた部分です。しかしこれだとうまくいきません。そこでモデルのシェーダーもuvをuv2にかえてみたら書き込み自体はうまくいくのですが、今度はモデルにうまくメインテクスチャが張り付けられていません。
ああああ
左が通常のUnlitシェーダーで、右はuv2にかえたものです。御覧の通りおかしくなっています。

ライトマップの仕組みがよくわからないのですが、上のサイトによると重ならないようなUV配置を自動でつくるのがGenerate Light UVsなのだという理解です。UVを重ねるというのは、無駄な領域を減らしテクスチャサイズを減らすのが目的だと思うので、どうあがいてもテクスチャ一個だと「重ならないようにUV再配置をし、かつそれぞれの頂点は同じ位置を参照する」ことはできないと思うので、重ならないようにUV配置したとき参照する位置はバラバラ(=見た目がおかしくなる)のは必然といえば必然だと思うのですが、しかし上記のサイトでは画像を見る限りUV2を使いつつもテクスチャはちゃんと想定通りに貼ってあるようにみえます。これをどうやっているのか知りたいです。(上記のサイトではCommandBuffer.DrawRendererではなくGraphics.DrawMeshNowで行っていたので実際に書いてある通りやってみましたがうまくいかず...)

シェーダーコード

Shader "Painter/ProjectivePaint"
{
    Properties
    {
        _BrushColor("Brush Color", Color) = (1, 1, 1, 1)
        _BrushTex("Projection Texture", 2D) = "white" { }
        _DepthTex("Depth Texture", 2D) = "white" { }
    }

        SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 100

        Pass
        {
        // for back facing triangle
        Cull Off

        CGPROGRAM

        #pragma vertex vert
        #pragma fragment frag

        #include "UnityCG.cginc"

        struct appdata
        {
            float4 vertex: POSITION;
            float2 uv : TEXCOORD0;
            float2 uv2 : TEXCOORD1;    // use the lightmapping uv
        };

        struct v2f
        {
            float2 uv: TEXCOORD0;
            float2 uv2 : TEXCOORD1;
            float4 vertex: SV_POSITION;
            float4 projUV: TEXCOORD2;
        };

        sampler2D _ModelMainTex; //CommandBuffer.SetGlobalTextureを使いメインテクスチャをセットしている
        sampler2D _BrushTex;
        sampler2D _DepthTex;
        float4 _ModelMainTex_ST;
        float4 _BrushColor;

        uniform float4x4 ProjectorVPMat;

        v2f vert(appdata v)
        {
            v2f o;

            // regard uv as a vertex position when painting 
            float2 position = v.uv2 * 2.0 - 1.0; /// uv2を使用
            #if UNITY_UV_STARTS_AT_TOP
            // reverse y if DirectX
            position.y *= -1.0;
            #endif
            o.vertex = float4(position, 0.0, 1.0);

            float4x4 mvpMat = mul(ProjectorVPMat, unity_ObjectToWorld);
            o.projUV = ComputeGrabScreenPos(mul(mvpMat, v.vertex));
            //o.uv = TRANSFORM_TEX(v.uv, _ModelMainTex);
            o.uv = v.uv;
            o.uv2 = v.uv2;
            return o;
        }

        fixed4 frag(v2f i) : SV_Target
        {
            /// 本質問にあまり関係ないデプスの処理
            #ifdef UNITY_REVERSED_Z
            float inverseZ = 1.0;
            #else
            float inverseZ = 0.0;
            #endif

            fixed4 projColor = fixed4(0, 0, 0, 0);
            float depth = 1.0 - inverseZ;

            if (i.projUV.w > 0.0)
            {
                // projection to screen space
                i.projUV /= i.projUV.w;

                if (i.projUV.x >= 0 && i.projUV.x <= 1 && i.projUV.y >= 0 && i.projUV.y <= 1)
                {
                    // sample the main color and depth at the same position
                    projColor = tex2D(_BrushTex, i.projUV);
                    depth = tex2D(_DepthTex, i.projUV).r;
                }
            }

            // make the range of both the clip coordinate z and depth [0, 1]

            float near = UNITY_NEAR_CLIP_VALUE;
            float far = 1.0 - inverseZ;
            float normalizedZ = (i.projUV.z - near) / (far - near);

            #ifdef UNITY_REVERSED_Z
            float normalizedDepth = 1 - depth;
            #else
            float normalizedDepth = depth;
            #endif


            // discard the current pixel if depth is less than the current z coordinate
            float avoidZFighting = 0.001;
            clip(normalizedDepth - normalizedZ + avoidZFighting);
       /// デプスの処理終わり

            fixed4 mainColor = tex2D(_ModelMainTex, i.uv2); // uv2で色を取得

            // if _BrushColor.a = 0 or projColor.a = 0, then mainColor. if both 1, then _BrushColor.
            return mainColor * (1 - _BrushColor.a * projColor.a) + _BrushColor * _BrushColor.a * projColor.a;
        }
        ENDCG
    }
    }
}

以上です。よろしくお願いします。

追記

Bongoさんが実装されていたようにUV2用のテクスチャを動的に作成し、uvをuv2に置き換えてみたところ正常にテクスチャがマッピングされ、かつペイントができるようになりましたが、ここで新たな問題として、テクスチャが劣化し以下のように黒い線が目立つようになりました。

イメージ説明

これをどうにかできないか考えていたのですが、少し改善できたので報告します。
edo_m18さんの記事でも紹介されている、Unity Graphics Programming vol.2(有料)
こちらにもペイントのプロジェクトがあり、参考になるものがないかと探していたのですが、「ペイントした際できてしまうメッシュの隙間を埋めるシェーダー」を使用していて、ソースコードはGithubで公開されていました。
ソース
こちらでは、ピクセルの色のアルファが一定より低ければ近傍の色を使う、といった実装になっています。試しにこれを使ってみましたら、多少改善したのですが、メインカメラがMSAAありだとまだ黒い線が目立っています。

イメージ説明

そこで、少し遠くの近傍テクセルをとってくるようにしたら以下のように。

float2 d = _MainTex_TexelSize.xy * 5;


イメージ説明
手にはまだ少し黒い線が残ってますが、顔のはほとんど見えません。

なんとか黒い線をなんとか少なくすることはできたのですが、UV2テクスチャにした時点で以下のような劣化も発生しており、これを直す方法はまだわかりません。
イメージ説明
Unityちゃんの服やリボンなどに変な模様が...
イメージ説明
テクスチャには変な模様はないのですが、モデルをみるとついています。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 5

+1

ライトマップ用UVに関する解釈はおっしゃる通りかと思います。UVを使ってサンプリングすることを前提としたテクスチャをUV2を使ってサンプリングすれば、めちゃくちゃな色で塗られてしまうことになるでしょう。

初期化部分で、下記のようなオブジェクトのメインテクスチャを内容を複製したレンダーテクスチャに差し替える部分はまだ残っているでしょうか?

var mainTex = objMat.mainTexture;
// 省略
modelMainRT = new RenderTexture(mainTex.width, mainTex.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default);
// 省略
Graphics.Blit(mainTex, modelMainRT);
objMat.mainTexture = modelMainRT;

もしここが上記の状態のままですと、UV前提のテクスチャの内容がそのままmodelMainRTに複製されてしまい、それ以降modelMainRTからUV2を使ってサンプリングするとおかしなことになるはずです。
仮に、下図のようなUV(わざと側面投影にし、左右対称になるようにしました)およびUV2を持つスザンヌの場合...

UV、UV2

modelMainRTは下図のように再配置されるべきでしょう。

テクスチャ再配置

そこで、下記のようなシェーダーを作成し...

Shader "Unlit/UVToUV2"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Cull Off
            ZWrite Off
            ZTest Always

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float2 uv : TEXCOORD0;
                float2 uv2 : TEXCOORD1;
            };

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert(appdata v)
            {
                v2f o;

                // 画面上の頂点の位置はUV2をもとに配置し...
                float2 position = v.uv2 * 2.0 - 1.0;
                #if UNITY_UV_STARTS_AT_TOP
                position.y *= -1.0;
                #endif
                o.vertex = float4(position, 0.0, 1.0);

                // テクスチャはUVの位置からサンプリングする
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

modelMainRT初期化部分を下記のようにすると...

// 単純なBlitの代わりに...
// Graphics.Blit(mainTex, modelMainRT);

// テクスチャの各部位をUV2に合わせた位置に再配置しながらコピーする
var remapper = new Material(Shader.Find("Unlit/UVToUV2"));
remapper.mainTexture = mainTex;
var c = new CommandBuffer {name = "Remap modelMainTex"};
c.SetRenderTarget(modelMainRT);
c.DrawRenderer(renderer, remapper);
Graphics.ExecuteCommandBuffer(c);

おそらく見た目はおおよそ正常化するでしょう。

結果

ですが、よく見るとテクスチャのピクセルが目立っており、品質が低下しています。UV2においてそれぞれのポリゴンがテクスチャ中を占める面積はUVより小さいでしょうから、modelMainRTの解像度は元のテクスチャよりも大きくしてやった方がいいかと思います。
また、ポリゴン境界にうっすら黒い線が現れてしまうかもしれません。未実験ですが、UVToUV2にジオメトリーシェーダーパートを組み込み、まず各ポリゴンを少しだけ太らせてからレンダリング、その後ポリゴンを太らせずにレンダリングすることで、ポリゴン境界外側にちょっと色を漏れ出させてやれば緩和できるかもしれません。

元のテクスチャを下絵にしてペイントを描き足していくという現状のやり方ですと、UV2を使ったときの品質低下はいくらかは妥協しなければならないような気がします。ご提示の記事からはedo_m18さんの場合どのように実現しているかは読み取れませんでしたが、改善案としては下絵とペイントを分離してしまう手が考えられるでしょう。

最初にmodelMainRTに元のテクスチャを下絵としてコピーするのはやめてまっさらな透明とし、ブラシによるペイントだけを描き足していくようにして(もはやメインテクスチャではなくなるので、ついでに名称もpaintRTとでも変えてしまってもいいでしょう)、そしてモデルのマテリアルでは下絵部分はUVを使ってメインテクスチャからサンプリング、ペイント部分はUV2を使ってペイントテクスチャからサンプリング、そしてこれら2つの色を合成して出力色とする感じです(テクスチャペイントではなくデカール表現に関する記事ですが、プロ生ちゃん付属のToon Shaderにデカール機能を足して血糊表現とかしてみる。 - Onoty3Dに掲載されている製作例のToonLitDecal.shaderのsurf中で行っているような合成法がご参考になりそうです)。これなら品質低下はペイントテクスチャだけにとどめられるのではないでしょうか?

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/15 09:46

    こんにちわ。少し改善できたのですが、画像があったほうがわかりやすいかなと思ったので質問文に追記しました。
    黒い線はけっこう改善できたのですが他にも問題が。UV2テクスチャをつくると発生するのでテクスチャ劣化の問題だと思うのですが、新たに作成したUV2用のレンダーテクスチャを見ても変な模様はなく、シーンを見ると変な模様がついているのが謎です。

    提案されたジオメトリシェーダを使うやり方はよくわからなくてまだできてないです。。。

    キャンセル

  • 2019/04/15 10:06

    ジオメトリーシェーダーは一案として口走っただけですので、他の方法でも問題ないでしょう。どうやらいい感じにポリゴン境界を埋めることができているようですので、このやり方でいいと思います。

    変な模様の件ですが、パターンが腰回りのヒラヒラ(スカート?)に似ていますね。他にも、袖の内側にジャケット側面の市松模様らしきパターンが現れているようです。
    もしかして、法線マップもUV2でサンプリングしてしまってはいないでしょうか?

    キャンセル

  • 2019/04/15 10:43

    あ、なるほど。C#でMeshのUVをUV2に変えてしまっているのですが、そうするとメインテクスチャはUV2用のレンダーテクスチャに差し替えられているから問題ないが他のテクスチャはUV前提なので問題がある、ということですかね。

    こうなるともう全てのテクスチャをUV2で使えるレンダーテクスチャに差し替えるしかなくなってしまいますね。。。とりあえずやってみます。

    キャンセル

+1

追記

下図のような理屈に基づき、塗り漏らし軽減ができないか試してみました。

色漏れ出し軽減案

UnitychanPaint/Brushは下記のように変更し...

Shader "UnitychanPaint/Brush"
{
    Properties
    {
        [HideInInspector] _BrushColor ("Brush Color", Color) = (1, 1, 1, 1)
        [HideInInspector] _BrushTex ("Brush Texture", 2D) = "white" {}
        [HideInInspector] _DepthTex ("Depth Texture", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        Pass
        {
            Lighting Off
            Cull Off
            ZWrite Off
            ZTest Always
            Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha

            // ステンシルを追加
            Stencil
            {
                Ref 1
                ReadMask 1
                WriteMask 1
                Comp Greater
                Pass Replace
            }

            CGPROGRAM

            #pragma vertex vert
            #pragma geometry geom // geomを追加
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex: POSITION;
                float2 uv4: TEXCOORD3;
            };

            struct v2f
            {
                float4 brushUV: TEXCOORD0;
                float4 vertex: SV_POSITION;
            };

            float4 _BrushColor;
            sampler2D _BrushTex;
            sampler2D _DepthTex;
            float4x4 _BrushVPMat;
            float2 _VertexOffset; // オフセット量を追加

            v2f vert(appdata v)
            {
                v2f o;

                float2 position = v.uv4 * 2.0 - 1.0;
                #if UNITY_UV_STARTS_AT_TOP
                    position.y *= -1.0;
                #endif
                o.vertex = float4(position, 0.0, 1.0);

                float4x4 mvpMat = mul(_BrushVPMat, unity_ObjectToWorld);
                o.brushUV = ComputeGrabScreenPos(mul(mvpMat, v.vertex));
                return o;
            }

            [maxvertexcount(3)]
            void geom(triangle v2f i[3], inout TriangleStream<v2f> oStream)
            {
                // 3頂点を入力として受け取り、それぞれ重心から遠ざかる向きに_VertexOffsetに応じた量だけずらしています
                // ポリゴンの角が鋭角なほど大きく引っ張るようにして、エッジの太り方が一様になるようにしました
                // 頂点部分をあまり長く引っ張り出すと、他のポリゴンから色を漏れ出させるべき領域を侵食してしまうリスクが
                // 大きくなるでしょうから、3頂点ではなく6個くらい頂点を出力して、角の部分は面取りしてやった方が
                // 望ましいかもしれません
                float2 p0 = i[0].vertex.xy;
                float2 p1 = i[1].vertex.xy;
                float2 p2 = i[2].vertex.xy;
                float2 d01 = normalize(p1 - p0);
                float2 d12 = normalize(p2 - p1);
                float2 d20 = normalize(p0 - p2);
                float2 amount0 = _VertexOffset / max(_VertexOffset, sqrt((1.0 - dot(d01, -d20)) * 0.5));
                float2 amount1 = _VertexOffset / max(_VertexOffset, sqrt((1.0 - dot(d12, -d01)) * 0.5));
                float2 amount2 = _VertexOffset / max(_VertexOffset, sqrt((1.0 - dot(d20, -d12)) * 0.5));
                float2 center = (p0 + p1 + p2) / 3.0;
                v2f o;
                o.vertex = float4(p0 + normalize(p0 - center) * amount0, 0.0, 1.0);
                o.brushUV = i[0].brushUV;
                oStream.Append(o);
                o.vertex = float4(p1 + normalize(p1 - center) * amount1, 0.0, 1.0);
                o.brushUV = i[1].brushUV;
                oStream.Append(o);
                o.vertex = float4(p2 + normalize(p2 - center) * amount2, 0.0, 1.0);
                o.brushUV = i[2].brushUV;
                oStream.Append(o);
            }

            fixed4 frag(v2f i): SV_Target
            {
                #ifdef UNITY_REVERSED_Z
                    float inverseZ = 1.0;
                #else
                    float inverseZ = 0.0;
                #endif

                float brushAlpha = 0.0;
                float depth = 1.0 - inverseZ;

                if (i.brushUV.w > 0.0)
                {
                    i.brushUV /= i.brushUV.w;

                    if (i.brushUV.x >= 0 && i.brushUV.x <= 1 && i.brushUV.y >= 0 && i.brushUV.y <= 1)
                    {
                        brushAlpha = tex2D(_BrushTex, i.brushUV).a;
                        depth = tex2D(_DepthTex, i.brushUV).r;
                    }
                }

                float near = UNITY_NEAR_CLIP_VALUE;
                float far = 1.0 - inverseZ;
                float normalizedZ = (i.brushUV.z - near) / (far - near);

                #ifdef UNITY_REVERSED_Z
                    float normalizedDepth = 1 - depth;
                #else
                    float normalizedDepth = depth;
                #endif

                float avoidZFighting = 0.001;
                clip(normalizedDepth - normalizedZ + avoidZFighting);

                return fixed4(_BrushColor.rgb, _BrushColor.a * brushAlpha);
            }
            ENDCG

        }
    }
}

Painteeを下記のように変更しました。

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;

namespace UnitychanPaint
{
    public class Paintee : MonoBehaviour
    {
        private static readonly int PaintTexId = Shader.PropertyToID("_PaintTex");
        private static readonly int VertexOffsetId = Shader.PropertyToID("_VertexOffset"); // プロパティID追加

        [SerializeField] private Vector2Int paintTextureSize = new Vector2Int(2048, 2048);
        [SerializeField] private RenderTexture paintRt;

        private CommandBuffer renderToTextureCommand;
        private CommandBuffer renderToScreenCommand;
        private IEnumerable<Renderer> renderers;
        private Brush brush;

        // 省略

        private void InitTextures()
        {
            this.paintRt = new RenderTexture(
                this.paintTextureSize.x,
                this.paintTextureSize.y,
                24, // 24ビットデプスにするとステンシルが使用可能になる
                RenderTextureFormat.ARGB32,
                RenderTextureReadWrite.Default) {name = "Paint"};
            this.paintRt.Create();
        }

        // 省略

        private void UpdateCommands(Brush b)
        {
            var brushMat = b.BrushMaterial;
            var paintMat = b.PaintMaterial;
            var r2t = this.renderToTextureCommand;
            var r2s = this.renderToScreenCommand;
            var unitVertexOffset = Vector2.one / this.paintTextureSize;
            r2t.Clear();
            r2t.ClearRenderTarget(true, false, Color.clear); // ステンシルをクリア
            r2s.Clear();
            r2s.SetGlobalTexture(PaintTexId, this.paintRt);

            for (var j = 0; j < 2; j++)
            {
                // r2tにおいて、ポリゴン膨張なし塗り→3テクセル膨張塗りの2回塗りを行うように変更
                // 色漏れ出し幅を広くする場合、一度に膨張させる幅を増やすよりも反復回数を増やした方が
                // 描画コストはかかるものの、きれいな仕上がりになると思います
                r2t.SetGlobalVector(VertexOffsetId, unitVertexOffset * (j * 3));

                foreach (var r in this.renderers)
                {
                    // 手抜きして元のコードを残したため、2反復の中で毎回subMeshCountを求めています...
                    var subMeshCount = 0;
                    switch (r)
                    {
                        case MeshRenderer mr:
                            subMeshCount = mr.GetComponent<MeshFilter>().sharedMesh.subMeshCount;
                            break;
                        case SkinnedMeshRenderer smr:
                            subMeshCount = smr.sharedMesh.subMeshCount;
                            break;
                        default:
                            subMeshCount = 1;
                            break;
                    }

                    for (var i = 0; i < subMeshCount; i++)
                    {
                        r2t.DrawRenderer(r, brushMat, i);
                    }

                    if (j > 0)
                    {
                        continue;
                    }

                    for (var i = 0; i < subMeshCount; i++)
                    {
                        r2s.DrawRenderer(r, paintMat, i);
                    }
                }
            }
        }

        // 省略
    }
}

私の場合ですと、3テクセル分ぐらい膨張させると塗り漏らしがかなり目立たなくなりました。
ほっぺに少々塗り漏らしがありますが、もっと精度よくする場合は膨張量を2~1テクセルに減らし、代わりにfor (var j = 0; j < 2; j++)の部分の繰り返し数を3回、4回...と増やすと、コード内のコメントで言及しました「他のポリゴンから色を漏れ出させるべき余地を侵食してしまう現象」が起こりにくくなるんじゃないかと思います。レンダリング1回でかなり描画コストの発生するようなハイポリゴンでなければ、レンダリング回数のちょっとした増加ぐらいでは大した問題にならないんじゃないでしょうか。

膨張なし
膨張なし

3テクセル膨張
3テクセル膨張

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/29 23:50

    おお、なるほど...
    最初におっしゃってたジオメトリシェーダを使ったやり方、こんな感じになるんですね。膨張させたら塗りがおかしくなりそうだなと思ってたのですが、ここはステンシルで解決するんですね。ありがとうございます。

    キャンセル

checkベストアンサー

0

  • MouseOperatorの続き
    public class PreprocessUpdateInfoObservable : OperatorObservableBase<MouseOperator.UpdateInfo>
    {
        private readonly IObservable<MouseOperator.UpdateInfo> source;

        public PreprocessUpdateInfoObservable(IObservable<MouseOperator.UpdateInfo> source) : base(
            source.IsRequiredSubscribeOnCurrentThread())
        {
            this.source = source;
        }

        protected override IDisposable SubscribeCore(IObserver<MouseOperator.UpdateInfo> observer, IDisposable cancel)
        {
            return new PreprocessUpdateInfo(this, observer, cancel).Run();
        }

        // count only
        private class PreprocessUpdateInfo : OperatorObserverBase<MouseOperator.UpdateInfo, MouseOperator.UpdateInfo>
        {
            private bool initialized;
            private readonly PreprocessUpdateInfoObservable parent;
            private MouseOperator.UpdateInfo prev;

            public PreprocessUpdateInfo(
                PreprocessUpdateInfoObservable parent,
                IObserver<MouseOperator.UpdateInfo> observer,
                IDisposable cancel) : base(observer, cancel)
            {
                this.parent = parent;
            }

            public override void OnCompleted()
            {
                try
                {
                    this.observer?.OnCompleted();
                }
                finally
                {
                    this.Dispose();
                }
            }

            public override void OnError(Exception error)
            {
                try
                {
                    this.observer?.OnError(error);
                }
                finally
                {
                    this.Dispose();
                }
            }

            public override void OnNext(MouseOperator.UpdateInfo value)
            {
                if (!this.initialized)
                {
                    this.prev = value;
                    this.initialized = true;
                }
                else
                {
                    var p = this.prev;
                    value.MouseButtonDown = !p.MouseButton && value.MouseButton;
                    value.MouseButtonUp = p.MouseButton && !value.MouseButton;
                    value.SpaceDown = !p.Space && value.Space;
                    value.SpaceUp = p.Space && !value.Space;
                    value.ShiftDown = !p.Shift && value.Shift;
                    value.ShiftUp = p.Shift && !value.Shift;
                    value.PointerOverUiEnter = !p.IsPointerOverUi && value.IsPointerOverUi;
                    value.PointerOverUiExit = p.IsPointerOverUi && !value.IsPointerOverUi;
                    this.prev = value;
                    this.observer?.OnNext(value);
                }
            }

            public IDisposable Run()
            {
                return this.parent.source.Subscribe(this);
            }
        }
    }

    public static class IObservableExtensions
    {
        public static IObservable<MouseOperator.UpdateInfo> PreprocessUpdateInfo(
            this IObservable<MouseOperator.UpdateInfo> source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            return new PreprocessUpdateInfoObservable(source);
        }
    }
}

また、省略しましたがUI上でブラシの設定を操作するためのColorSelectorSizeSelectorがあります。

ヒエラルキー

マテリアルはテクスチャ描き込み用のUnitychanPaint/Brushと、画面描画用のUnitychanPaint/Paintに分けることにしました。共通したコードがけっこうあるので、cgincファイルにまとめればスマートになるかもしれません。
ブラシをモデルに投影する機構はこれまでのものと同様です。

Shader "UnitychanPaint/Brush"
{
    Properties
    {
        [HideInInspector] _BrushColor ("Brush Color", Color) = (1, 1, 1, 1)
        [HideInInspector] _BrushTex ("Brush Texture", 2D) = "white" {}
        [HideInInspector] _DepthTex ("Depth Texture", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        Pass
        {
            Lighting Off
            Cull Off
            ZWrite Off
            ZTest Always
            Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex: POSITION;
                float2 uv4: TEXCOORD3;
            };

            struct v2f
            {
                float4 brushUV: TEXCOORD0;
                float4 vertex: SV_POSITION;
            };

            float4 _BrushColor;
            sampler2D _BrushTex;
            sampler2D _DepthTex;
            float4x4 _BrushVPMat;

            v2f vert(appdata v)
            {
                v2f o;

                float2 position = v.uv4 * 2.0 - 1.0;
                #if UNITY_UV_STARTS_AT_TOP
                    position.y *= -1.0;
                #endif
                o.vertex = float4(position, 0.0, 1.0);

                float4x4 mvpMat = mul(_BrushVPMat, unity_ObjectToWorld);
                o.brushUV = ComputeGrabScreenPos(mul(mvpMat, v.vertex));
                return o;
            }

            fixed4 frag(v2f i): SV_Target
            {
                #ifdef UNITY_REVERSED_Z
                    float inverseZ = 1.0;
                #else
                    float inverseZ = 0.0;
                #endif

                float brushAlpha = 0.0;
                float depth = 1.0 - inverseZ;

                if (i.brushUV.w > 0.0)
                {
                    i.brushUV /= i.brushUV.w;

                    if (i.brushUV.x >= 0 && i.brushUV.x <= 1 && i.brushUV.y >= 0 && i.brushUV.y <= 1)
                    {
                        brushAlpha = tex2D(_BrushTex, i.brushUV).a;
                        depth = tex2D(_DepthTex, i.brushUV).r;
                    }
                }

                float near = UNITY_NEAR_CLIP_VALUE;
                float far = 1.0 - inverseZ;
                float normalizedZ = (i.brushUV.z - near) / (far - near);

                #ifdef UNITY_REVERSED_Z
                    float normalizedDepth = 1 - depth;
                #else
                    float normalizedDepth = depth;
                #endif

                float avoidZFighting = 0.001;
                clip(normalizedDepth - normalizedZ + avoidZFighting);

                return fixed4(_BrushColor.rgb, _BrushColor.a * brushAlpha);
            }
            ENDCG

        }
    }
}
Shader "UnitychanPaint/Paint"
{
    Properties
    {
        _Glossiness ("Smoothness", Range(0.0, 1.0)) = 0.85
        _Metallic ("Metallic", Range(0.0, 1.0)) = 0.0
        _AmbientBoost ("Ambient Boost", Range(0.0, 1.0)) = 0.5
        [HideInInspector] _BrushColor ("Brush Color", Color) = (1, 1, 1, 1)
        [HideInInspector] _BrushTex ("Brush Texture", 2D) = "white" {}
        [HideInInspector] _DepthTex ("Depth Texture", 2D) = "white" {}
        [HideInInspector] _DrawBrushSpot ("Draw Brush Spot", Float) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }

        Cull Back
        ZWrite Off
        ZTest LEqual

        CGPROGRAM
        #pragma surface surf Paint vertex:vert decal:blend exclude_path:deferred exclude_path:prepass nometa
        #pragma target 3.0
        #include "UnityPBSLighting.cginc"

        sampler2D _PaintTex;
        float4 _BrushColor;
        sampler2D _BrushTex;
        sampler2D _DepthTex;
        float4x4 _BrushVPMat;
        fixed _DrawBrushSpot;
        half _Glossiness;
        half _Metallic;
        half _AmbientBoost;

        struct Input
        {
            float2 uv4_PaintTex;
            float4 brushUV;
        };

        inline half4 LightingPaint(SurfaceOutputStandard s, half3 viewDir, UnityGI gi)
        {
            return LightingStandard(s, viewDir, gi);
        }

        inline void LightingPaint_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi)
        {
            LightingStandard_GI(s, data, gi);

            // 通常のレンダリング時と違って環境光が効かないらしく、陰の部分が真っ暗になってしまうようでした
            // そこで、環境光強度を無理やり増やして見た目を調節できるようにしました
            gi.indirect.diffuse += _AmbientBoost;
        }

        void vert(inout appdata_full v, out Input o) {
            UNITY_INITIALIZE_OUTPUT(Input,o);
            float4x4 mvpMat = mul(_BrushVPMat, unity_ObjectToWorld);
            o.brushUV = ComputeGrabScreenPos(mul(mvpMat, v.vertex));
        }

        void surf(Input IN, inout SurfaceOutputStandard o)
        {
            #ifdef UNITY_REVERSED_Z
                float inverseZ = 1.0;
            #else
                float inverseZ = 0.0;
            #endif

            float brushAlpha = 0.0;
            float depth = 1.0 - inverseZ;

            if (IN.brushUV.w > 0.0)
            {
                IN.brushUV /= IN.brushUV.w;

                if (IN.brushUV.x >= 0 && IN.brushUV.x <= 1 && IN.brushUV.y >= 0 && IN.brushUV.y <= 1)
                {
                    brushAlpha = tex2D(_BrushTex, IN.brushUV).a;
                    depth = tex2D(_DepthTex, IN.brushUV).r;
                }
            }

            float near = UNITY_NEAR_CLIP_VALUE;
            float far = 1.0 - inverseZ;
            float normalizedZ = (IN.brushUV.z - near) / (far - near);

            #ifdef UNITY_REVERSED_Z
                float normalizedDepth = 1 - depth;
            #else
                float normalizedDepth = depth;
            #endif

            float avoidZFighting = 0.001;
            float depthClipping = step(0.0, normalizedDepth - normalizedZ + avoidZFighting);

            float4 brush = float4(_BrushColor.rgb, _BrushColor.a * brushAlpha * depthClipping * _DrawBrushSpot);
            float4 paint = tex2D(_PaintTex, IN.uv4_PaintTex);
            paint.rgb /= max(paint.a, 0.001);
            float3 color = lerp(paint.rgb, brush.rgb, brush.a);
            float alpha = brush.a + paint.a * (1.0 - brush.a);

            #ifndef UNITY_COLORSPACE_GAMMA
                color = GammaToLinearSpace(color);
            #endif

            o.Albedo = color;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = alpha;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

動かしてみると下図のようになりました。

結果1

ご質問者さんの「ポリゴン境界黒線問題」と同様の原因による「ポリゴン境界塗り漏らし問題」がありますが、これは未対策です。ペイントテクスチャに塗られた色には透明部分がありますので、透明度によるポリゴン境界埋めはやりにくいかもしれません。ステンシル機能をうまく使えば可能な気もしますが...

結果2

また、最終的に作りたい「メインテクスチャ上にペイントが重ね塗りされたテクスチャ」の出力機能も未実装です。ポリゴン境界問題はこのタイミングで処理するのも一案かもしれません。たまにしか行わない処理でしょうから、隙間埋め処理が複雑で多少時間がかかったとしてもさほど問題ないかと思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/26 19:12 編集

    返信だいぶ遅くなりすみません。自分でシーン試して実行してみたのですが、うまくいきました。実装方法ですが以下の認識であっているでしょうか?

    塗り専用のレンダーテクスチャをテクスチャアトラスの要領で全体にひとつだけ作成する。これはUV2でのテクスチャである。MouseOperatorでマウス左クリックが押された時Brushシェーダーを使ってこのテクスチャに塗りこみを行う。

    モデルの描画は、通常のモデルのレンダリングが終了したあと、Paintシェーダーを使って上塗りする。これはsurfシェーダーで、ライティングが適用される。上塗り時には、塗り用のレンダーテクスチャからUV2(UV4)をつかってペイント部分の色をとってくる。左クリックしてない間もプレビューとしてマウスカーソルのあたりにブラシの形と色が表示されるが、これもこのシェーダー内で行われている。

    いろいろと参考になる部分があったのですが、特にテクスチャアトラスの部分は他の実装でも応用できそうな気がしました。この実装だとそれぞれのテクスチャについて黒い線ができないようにうまい具合にUV2専用テクスチャを作成する、なんて複雑なことしなくても済みますね。

    しかし、この実装だと編集最中はスムーズにいきますが、最後の出力部分が大変そうです。単純にテクスチャに書き込んでしまうと、編集時はPaintシェーダーでどのようにレンダリングされるか決まっていたのが、出力したらモデルのシェーダーになるので見た目が塗っている時と変わってしまった、となりそうです。というか、出力時にテクスチャに塗りこむならば結局UVをUV2にいれかえ、UV2用のテクスチャをそれぞれつくらなければなりません。なかなか難しいですね。最初からUV2用のを作成するか、後から作成するか、どちらにせようまいこと劣化しないように(劣化してないように見せるように)する方法を頑張って考えなければならなそうです。

    とりあえず、最初に疑問だったUV2を使って重複しないように塗るという疑問は解決したので解決済みにしておきます。本当にありがとうございます。

    ステンシルは最初黒線問題を考えているときにちらりと頭にうかんだのですが、うまい方法が思いつきませんでした。あれはレンダーされた場所、されてない場所を明らかにし、その結果を使うということですよね。しかし黒線が現れるレンダラーの境界がレンダーされてない場所になっているわけでもないと思うので難しいのかなと思いました。もし可能そうであればアイデアだけでも教えてほしいです。

    キャンセル

  • 2019/04/27 22:39

    最終的にペイント完了結果を元のテクスチャと統合して出力したい、しかもペイント対象のモデルを任意に選べるようにしたい、という条件があるのがなかなかやっかいですね。
    3D画面上での表示だけがペイントされているように見えれば十分ならペイントだけ別テクスチャ方式がよさそうですし、逆にモデルのデザインに「UVの重なり・はみ出し禁止」と縛りを与えることができるならメインテクスチャへの直接描き込み方式が楽そうですし...悩ましい問題だと思います。

    黒線問題についてですが、後で考え直してみたら「注目テクセルの周囲のテクセルの透明度を調べてエッジ拡張」方式とステンシルを組み合わせてもいまいちのような気がしてきました。やるとしたら近傍ピクセルのステンシル値を取得する必要がありそうですが、それは無理っぽいので、結局ステンシルの代替となるようなテクスチャをもう一枚用意することになりそうで面倒そうです。
    以前に申し上げた「ジオメトリーシェーダーステージを追加してポリゴンを太らせる」の方がマシなように思われたので、ちょっと検討してみました。またもや文字数が限界に達してしまい別回答です。すみません...

    キャンセル

0

下絵となるモデル描画はモデル本来のレンダラーにまかせ、OnRenderObjectタイミングでペイントを上塗りする方式を試してみました。
塗り部分は独自のシェーダーで描画するため、塗り色もモデル本来のシェーダーで処理されるご質問者さんの方式と比べると、塗り色の質感がモデルの質感とマッチしない弱点があるかと思います。逆に塗り部分にだけペンキのような質感を与えられる利点があるとも言えなくもなさそうですが...

少々長くなってしまい、3回答に分割してしまいましたがご容赦ください。

スクリプトはブラシカメラにアタッチするBrushと塗られる側にアタッチするPainteeに分け、Brushはマウス処理担当のMouseOperatorからの操作によって状態を変化させ、PainteeBrushを参照して自身の上にペイントを上塗りする作りにしました。塗り用のマテリアルもBrushが持っており、Painteeはそれを取得して描画に使っています。

  • Paintee
    ユニティちゃんにアタッチするスクリプトで、これのpaintRtに塗りをペイントしていくことにしました。paintRtは透明背景で、モデルの色は乗せずに塗りだけを重ねていくようにしています。
    コマンドバッファもrenderToTextureCommandrenderToScreenCommandの2つに分け、3D画面上への塗りの上乗せはrenderToScreenCommandが担当、マウスボタンを押し下げているときはさらにrenderToTextureCommandpaintRtへの描き込みを行います。
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;

namespace UnitychanPaint
{
    public class Paintee : MonoBehaviour
    {
        private static readonly int PaintTexId = Shader.PropertyToID("_PaintTex");

        [SerializeField] private Vector2Int paintTextureSize = new Vector2Int(2048, 2048);
        [SerializeField] private RenderTexture paintRt;

        private CommandBuffer renderToTextureCommand;
        private CommandBuffer renderToScreenCommand;
        private IEnumerable<Renderer> renderers;
        private Brush brush;

        public Brush Brush
        {
            get => this.brush;
            set
            {
                this.brush = value;
                if (value != null)
                {
                    this.UpdateCommands(value);
                }
            }
        }

        public void RenderToTexture()
        {
            var b = this.Brush;
            if (b == null)
            {
                return;
            }

            Graphics.SetRenderTarget(this.paintRt);
            b.UpdateDepth();
            b.UpdateViewProjectionMatrix(b.BrushMaterial);
            Graphics.ExecuteCommandBuffer(this.renderToTextureCommand);
        }

        private void RenderToScreen()
        {
            var b = this.Brush;
            if (b == null)
            {
                return;
            }

            b.UpdateDepth();
            b.UpdateViewProjectionMatrix(b.PaintMaterial);
            Graphics.ExecuteCommandBuffer(this.renderToScreenCommand);
        }

        private void Awake()
        {
            this.renderers = this.GetComponentsInChildren<Renderer>();
            this.InitModels();
            this.InitTextures();
            this.InitCommands();
        }

        private void InitModels()
        {
            // 念のための処置としてオブジェクトの各レンダラー経由で全メッシュを取得、
            // それらメッシュを複製してオリジナルは元のまま残す
            // UV2には事前に面の重複がないようポリゴンが配置されており、また各メッシュの
            // ポリゴン配置はそれぞれUV空間全面を使っていることを想定する
            // (どうやら「Generate Lightmap UVs」で作ったUV2はこうなるようです
            // もしUV2が適切に作られていないモデルにも対応したい場合、このメソッド中で
            // さらに独自のUV展開処理を行うことになるでしょう)
            // モデル全体で一枚のペイントテクスチャを使うことにしたため、複数のメッシュがあるならば
            // 各メッシュのUV2を縮小してタイル上に並べ、全モデルで一枚のテクスチャを使っても
            // 面の重なりが発生しないようにする
            // また、「UVの働きを検証する – Tsumiki Tech Times|積木製作」(http://tsumikiseisaku.com/blog/how-uv-works/) に
            // よると、UV2やUV3は後でUnity側からいじられる恐れがあるため、念のためできあがったUV2は
            // UV4にセットして、ブラシ塗りや画面上の描画にもUV4を使う
            var rs = this.renderers;
            if (rs == null)
            {
                return;
            }

            var mrMeshPairs = rs.OfType<MeshRenderer>().Select(mr => (mr, mr.GetComponent<MeshFilter>().sharedMesh))
                .ToArray();
            var smrMeshPairs = rs.OfType<SkinnedMeshRenderer>().Select(smr => (smr, smr.sharedMesh)).ToArray();
            var meshToRenderer = new Dictionary<Mesh, (List<MeshRenderer>, List<SkinnedMeshRenderer>)>();
            foreach (var (r, m) in mrMeshPairs)
            {
                if (meshToRenderer.TryGetValue(m, out var element))
                {
                    element.Item1.Add(r);
                }
                else
                {
                    meshToRenderer.Add(m, (new List<MeshRenderer> {r}, new List<SkinnedMeshRenderer>()));
                }
            }

            foreach (var (r, m) in smrMeshPairs)
            {
                if (meshToRenderer.TryGetValue(m, out var element))
                {
                    element.Item2.Add(r);
                }
                else
                {
                    meshToRenderer.Add(m, (new List<MeshRenderer>(), new List<SkinnedMeshRenderer> {r}));
                }
            }

            var clones = new List<Mesh>();
            foreach (var element in meshToRenderer)
            {
                var clone = Instantiate(element.Key);
                clones.Add(clone);
                foreach (var meshRenderer in element.Value.Item1)
                {
                    meshRenderer.GetComponent<MeshFilter>().sharedMesh = clone;
                }

                foreach (var skinnedMeshRenderer in element.Value.Item2)
                {
                    skinnedMeshRenderer.sharedMesh = clone;
                }
            }

            var meshCount = clones.Count;
            if (meshCount == 0)
            {
                return;
            }

            var rows = 1;
            var columns = 1;
            while ((rows * columns) < meshCount)
            {
                columns++;
                if ((rows * columns) >= meshCount)
                {
                    break;
                }

                rows++;
            }

            Debug.Log($"Pack {meshCount} meshes into {rows} x {columns} atlas.");
            var index = 0;
            var uv2 = new List<Vector2>();
            var tileSize = new Vector2(1.0f / columns, 1.0f / rows);
            for (var j = 0; j < rows; j++)
            {
                for (var i = 0; i < columns; i++)
                {
                    var mesh = clones[index];
                    mesh.GetUVs(1, uv2);
                    var uvCount = uv2.Count;
                    var offset = new Vector2(i, j);
                    for (var k = 0; k < uvCount; k++)
                    {
                        uv2[k] = (uv2[k] + offset) * tileSize;
                    }
                    mesh.SetUVs(3, uv2);
                    index++;
                }
            }
        }

        private void InitTextures()
        {
            this.paintRt = new RenderTexture(
                this.paintTextureSize.x,
                this.paintTextureSize.y,
                0,
                RenderTextureFormat.ARGB32,
                RenderTextureReadWrite.Default) {name = "Paint"};
            this.paintRt.Create();
        }

        private void InitCommands()
        {
            // ゲームプレイ中にブラシを別のブラシオブジェクトに切り替えることがあるかも
            // しれないことを考慮し、初期化タイミングではコマンドバッファを生成するだけとして
            // 内容の構成はUpdateCommandsに分離した
            this.renderToTextureCommand = new CommandBuffer {name = "Render to texture"};
            this.renderToScreenCommand = new CommandBuffer {name = "Render to screen"};
        }

        private void UpdateCommands(Brush b)
        {
            var brushMat = b.BrushMaterial;
            var paintMat = b.PaintMaterial;
            var r2t = this.renderToTextureCommand;
            var r2s = this.renderToScreenCommand;
            r2t.Clear();
            r2s.Clear();
            r2s.SetGlobalTexture(PaintTexId, this.paintRt);
            foreach (var r in this.renderers)
            {
                var subMeshCount = 0;
                switch (r)
                {
                    case MeshRenderer mr:
                        subMeshCount = mr.GetComponent<MeshFilter>().sharedMesh.subMeshCount;
                        break;
                    case SkinnedMeshRenderer smr:
                        subMeshCount = smr.sharedMesh.subMeshCount;
                        break;
                    default:
                        subMeshCount = 1;
                        break;
                }

                for (var i = 0; i < subMeshCount; i++)
                {
                    r2t.DrawRenderer(r, brushMat, i);
                    r2s.DrawRenderer(r, paintMat, i);
                }
            }
        }

        private void OnRenderObject()
        {
            // モデルの描画が終わった後のタイミングでペイントを上塗りする
            if ((this.Brush == null) || (Camera.current == this.Brush.BrushCamera))
            {
                return;
            }

            this.RenderToScreen();
        }

        private void OnDestroy()
        {
            this.ReleaseTextures();
        }

        private void ReleaseTextures()
        {
            this.paintRt.Release();
        }
    }
}
  • Brush
    ブラシカメラにアタッチするスクリプトで、デプス用のテクスチャ類はこちらが持っています。ブラシカメラはメインカメラの子オブジェクトになっており、後述のMouseOperatorによってメインカメラの視点を変えると追従して回転、さらにマウスポインタの位置に合わせてローカル回転が操作され、ブラシの狙いを定めるようになっています。
using System;
using UniRx;
using UnityEngine;

namespace UnitychanPaint
{
    [RequireComponent(typeof(Camera))]
    public class Brush : MonoBehaviour
    {
        private static readonly int BrushColorId = Shader.PropertyToID("_BrushColor");
        private static readonly int BrushTexId = Shader.PropertyToID("_BrushTex");
        private static readonly int DepthTexId = Shader.PropertyToID("_DepthTex");
        private static readonly int BrushVpMatId = Shader.PropertyToID("_BrushVPMat");
        private static readonly int DrawBrushSpotId = Shader.PropertyToID("_DrawBrushSpot");

        [SerializeField] private Paintee target;
        [SerializeField] private Material brushMaterial; // 2D展開図上にペイントするためのマテリアル
        [SerializeField] private Material paintMaterial; // 3D画面上でモデル表面に塗りを上乗せするためのマテリアル
        [SerializeField] private Color brushColor;
        [SerializeField] private Texture brushShape;

        private Transform brushTransform;
        private Transform parentTransform;
        private RenderTexture colorRt;
        private RenderTexture depthRt;
        private bool needsDrawUnpaintedBrushSpot;

        public Camera BrushCamera { get; private set; }

        // ブラシ色
        public Color BrushColor
        {
            get => this.brushColor;
            set
            {
                this.brushColor = value;
                this.brushMaterial.SetColor(BrushColorId, value);
                this.paintMaterial.SetColor(BrushColorId, value);
            }
        }

        // ブラシテクスチャ(現状アルファしか使用していない)
        public Texture BrushShape
        {
            get => this.brushShape;
            set
            {
                this.brushShape = value;
                this.brushMaterial.SetTexture(BrushTexId, value);
                this.paintMaterial.SetTexture(BrushTexId, value);
            }
        }

        // ブラシの位置にブラシ形状を投影描画する場合はtrueにする
        // falseだと既にペイント済みのペイントテクスチャしか描画しない
        public bool NeedsDrawUnpaintedBrushSpot
        {
            get => this.needsDrawUnpaintedBrushSpot;
            set
            {
                this.needsDrawUnpaintedBrushSpot = value;
                this.paintMaterial.SetFloat(DrawBrushSpotId, value ? 1.0f : 0.0f);
            }
        }

        // 塗るターゲットとなるPaintee
        public Paintee Target
        {
            get => this.target;
            set
            {
                if (this.target != null)
                {
                    this.target.Brush = null;
                }

                if (value != null)
                {
                    value.Brush = this;
                }

                this.target = value;
            }
        }

        public Material BrushMaterial
        {
            get
            {
                if (this.brushMaterial == null)
                {
                    throw new InvalidOperationException($"{nameof(this.BrushMaterial)} is not set!");
                }

                return this.brushMaterial;
            }
        }

        public Material PaintMaterial
        {
            get
            {
                if (this.paintMaterial == null)
                {
                    throw new InvalidOperationException($"{nameof(this.PaintMaterial)} is not set!");
                }

                return this.paintMaterial;
            }
        }

        public void UpdateViewProjectionMatrix(Material material)
        {
            var v = this.BrushCamera.worldToCameraMatrix;
            var p = GL.GetGPUProjectionMatrix(this.BrushCamera.projectionMatrix, true);
            material.SetMatrix(BrushVpMatId, p * v);
        }

        public void UpdateDepth()
        {
            this.BrushCamera.Render();
        }

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

  • Brushの続き
        public void LookAt(Vector3 worldAimPosition)
        {
            var localAimPosition = this.parentTransform.InverseTransformPoint(worldAimPosition);
            this.brushTransform.localRotation = Quaternion.LookRotation(localAimPosition);
        }

        private void Awake()
        {
            this.BrushCamera = this.GetComponent<Camera>();
            this.BrushCamera.aspect = 1;
            this.BrushCamera.enabled = false;
            this.BrushCamera.orthographic = false;
            this.BrushCamera.renderingPath = RenderingPath.VertexLit;
            this.BrushCamera.clearFlags = CameraClearFlags.Depth;
            this.BrushCamera.depthTextureMode = DepthTextureMode.Depth;
            this.BrushCamera.allowHDR = false;
            this.BrushCamera.allowMSAA = false;
            this.BrushCamera.allowDynamicResolution = false;

            this.brushTransform = this.transform;
            this.parentTransform = this.brushTransform.parent;
            this.brushTransform.localPosition = Vector3.zero;
            this.brushTransform.localRotation = Quaternion.identity;

            this.InitTextures();
            this.InitMaterials();
        }

        private void Start()
        {
            if (this.target != null)
            {
                this.Target = this.target;
            }
        }

        private void InitTextures()
        {
            var depthBufferWidth = this.BrushShape.width;
            var depthBufferHeight = this.BrushShape.height;
            this.colorRt = new RenderTexture(
                depthBufferWidth,
                depthBufferHeight,
                0,
                RenderTextureFormat.ARGB32) {name = "BrushColor"};
            this.colorRt.Create();
            this.depthRt = new RenderTexture(
                depthBufferWidth,
                depthBufferHeight,
                24,
                RenderTextureFormat.Depth) {name = "BrushDepth"};
            this.depthRt.Create();
            this.BrushCamera.SetTargetBuffers(this.colorRt.colorBuffer, this.depthRt.depthBuffer);
        }

        private void InitMaterials()
        {
            this.brushMaterial.SetColor(BrushColorId, this.BrushColor);
            this.brushMaterial.SetTexture(BrushTexId, this.BrushShape);
            this.brushMaterial.SetTexture(DepthTexId, this.depthRt);
            this.paintMaterial.SetColor(BrushColorId, this.BrushColor);
            this.paintMaterial.SetTexture(BrushTexId, this.BrushShape);
            this.paintMaterial.SetTexture(DepthTexId, this.depthRt);
        }

        private void OnDestroy()
        {
            this.ReleaseTextures();
        }

        private void ReleaseTextures()
        {
            this.colorRt.Release();
            this.depthRt.Release();
        }
    }
}
  • MouseOperator
    本題のペイント機構には直接関係ありませんが、このスクリプトはヒエラルキーのルートにある単独のオブジェクトにアタッチされており、マウス操作に応じて視点やブラシを操作する作業を担当します。ボタン類の押下状態に応じて分岐する部分が整理されておらず、ごちゃごちゃしていますがご容赦ください...
using System;
using System.Linq;
using UniRx;
using UniRx.Operators;
using UniRx.Triggers;
using UnityEngine;
using UnityEngine.EventSystems;

namespace UnitychanPaint
{
    public class MouseOperator : MonoBehaviour
    {
        [SerializeField] private GameObject target;
        [SerializeField] private CanvasGroup uiCanvases;
        [SerializeField] private Brush brush;
        [SerializeField] private Texture2D textureCross;
        [SerializeField] private Texture2D textureGrabClosed;
        [SerializeField] private Texture2D textureGrabOpen;
        [SerializeField] private Vector2 hotSpot;
        [SerializeField] private float factorRotation;
        [SerializeField] private float factorZoom;
        private new Camera camera;
        private Transform cameraTransform;
        private Vector3 focus;
        private Vector3 previousMouseWorldPosition;

        private void Start()
        {
            if (Camera.main != null)
            {
                this.camera = Camera.main;
                this.cameraTransform = this.camera.transform;
            }

            Cursor.SetCursor(this.textureCross, this.hotSpot, CursorMode.ForceSoftware);

            // 初期注目点として、ターゲットのレンダラーのバウンディングボックスをすべて結合したボックスの中心点を選ぶ
            var t = this.target;
            if (t != null)
            {
                var renderers = t.GetComponentsInChildren<Renderer>();
                if (t.transform != null)
                {
                    this.focus = (renderers == null) || !renderers.Any()
                        ? t.transform.position
                        : renderers.Where(r => r != null).Select(r => r.bounds).Aggregate(
                            (u, b) =>
                            {
                                u.Encapsulate(b);
                                return u;
                            }).center;
                }
            }

            this.UpdateAsObservable().Select(
                _ =>
                {
                    var cam = this.camera;
                    var camTrans = this.cameraTransform;
                    var eveSys = EventSystem.current;
                    if ((cam == null) || (camTrans == null) || (eveSys == null))
                    {
                        throw new InvalidOperationException(
                            $"{nameof(cam)}:{cam} {nameof(camTrans)}:{camTrans} {nameof(eveSys)}:{eveSys}");
                    }

                    // 毎フレームマウスポインタの指す方角やマウスボタン・キーの押下状態を取得する
                    // さらに後段のPreprocessUpdateInfoで「UIにポインタが入った/出た」「キーが押された/離された」といった
                    // 状態変化を検出する
                    return new UpdateInfo
                    {
                        Camera = cam, CameraTransform = camTrans,
                        IsPointerOverUi = eveSys.IsPointerOverGameObject(),
                        MouseRay = cam.ScreenPointToRay(Input.mousePosition),
                        Shift = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift),
                        Space = Input.GetKey(KeyCode.Space),
                        MouseButton = Input.GetMouseButton(0)
                    };
                }).PreprocessUpdateInfo().Subscribe(
                info =>
                {
                    var camTransform = info.CameraTransform;
                    var camTransformPosition = camTransform.position;
                    var camTransformForward = camTransform.forward;

                    // マウスホイールでメインカメラを前後に移動
                    camTransformPosition += camTransformForward * Input.mouseScrollDelta.y * this.factorZoom;
                    camTransform.position = camTransformPosition;

                    // メインカメラを注目点に向ける
                    // カメラは傾いてしまいますが、ドラッグ視点操作に対する一貫性があるような気がして
                    // LookAtではなくFromToRotationを使用しました
                    camTransform.rotation =
                        Quaternion.FromToRotation(camTransformForward, this.focus - camTransformPosition) *
                        camTransform.rotation;

                    // 視点操作中はUIを半透明にし、使用不能にする
                    var uis = this.uiCanvases;
                    if (uis != null)
                    {
                        if (info.SpaceDown)
                        {
                            uis.interactable = false;
                            uis.alpha = 0.5f;
                        }
                        else if (info.SpaceUp)
                        {
                            uis.interactable = true;
                            uis.alpha = 1.0f;
                        }
                    }

                    // キーの状態やポインタがUI上にあるかどうかでマウスポインタのグラフィックを変更する
                    if (info.MouseButtonDown || info.MouseButtonUp || info.PointerOverUiEnter ||
                        info.PointerOverUiExit || info.SpaceDown || info.SpaceUp)
                    {
                        if (info.IsPointerOverUi && !info.Space)
                        {
                            Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
                        }
                        else
                        {
                            Cursor.SetCursor(
                                !info.Space ? this.textureCross :
                                info.MouseButton ? this.textureGrabClosed : this.textureGrabOpen,
                                this.hotSpot,
                                CursorMode.Auto);
                        }

                        this.brush.NeedsDrawUnpaintedBrushSpot = !info.Space;
                    }

                    // 現在マウスポインタが指しているワールド空間内の点を調べる
                    var forward = camTransform.forward;
                    var focusPlane = new Plane(-forward, this.focus);
                    focusPlane.Raycast(info.MouseRay, out var enter);
                    var mouseWorldPosition = info.MouseRay.GetPoint(enter);
                    if (info.MouseButtonDown)
                    {
                        this.previousMouseWorldPosition = mouseWorldPosition;
                    }

                    // スペースキーを押しながらドラッグで視点を回転、さらにシフトキー同時押しで
                    // 注視点とカメラを平行移動する
                    if (info.Space && info.MouseButton)
                    {
                        var mouseDelta = (mouseWorldPosition - this.previousMouseWorldPosition) / enter;
                        this.previousMouseWorldPosition = mouseWorldPosition;
                        if (info.Shift)
                        {
                            this.focus -= mouseDelta;
                            this.previousMouseWorldPosition -= mouseDelta;
                            camTransform.Translate(-mouseDelta, Space.World);
                        }
                        else
                        {
                            var rotationAxis = Vector3.Cross(forward, mouseDelta).normalized;
                            var rotationAngle = mouseDelta.magnitude * this.factorRotation;
                            camTransform.RotateAround(this.focus, rotationAxis, rotationAngle);
                        }

                        forward = camTransform.forward;
                        focusPlane = new Plane(-forward, this.focus);
                        focusPlane.Raycast(info.MouseRay, out enter);
                        mouseWorldPosition = info.MouseRay.GetPoint(enter);
                    }

                    // ブラシをマウスポインタの方角に向け、マウスボタンが押されていれば
                    // Painteeに対してペイントテクスチャへブラシを書き込むよう指示する
                    this.brush.LookAt(mouseWorldPosition);
                    if (!info.Space && !info.IsPointerOverUi && info.MouseButton)
                    {
                        var model = this.brush.Target;
                        if (model != null)
                        {
                            model.RenderToTexture();
                        }
                    }
                }).AddTo(this.gameObject);

            // 色が切り替えられたらブラシの色を変更
            var colorSelector = FindObjectOfType<ColorSelector>();
            if (colorSelector != null)
            {
                colorSelector.color.Subscribe(color => { this.brush.BrushColor = color; });
            }

            // サイズが変えられたらブラシカメラの画角を変更
            var sizeSelector = FindObjectOfType<SizeSelector>();
            if (sizeSelector != null)
            {
                sizeSelector.fieldOfView.Subscribe(fov => this.brush.BrushCamera.fieldOfView = fov);
            }
        }

        public struct UpdateInfo
        {
            public Camera Camera;
            public Transform CameraTransform;
            public Ray MouseRay;
            public bool Space;
            public bool SpaceDown;
            public bool SpaceUp;
            public bool Shift;
            public bool ShiftDown;
            public bool ShiftUp;
            public bool IsPointerOverUi;
            public bool PointerOverUiEnter;
            public bool PointerOverUiExit;
            public bool MouseButton;
            public bool MouseButtonDown;
            public bool MouseButtonUp;
        }
    }

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.92%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る