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

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

新規登録して質問してみよう
ただいま回答率
85.48%
Unity3D

Unity3Dは、ゲームや対話式の3Dアプリケーション、トレーニングシュミレーション、そして医学的・建築学的な技術を可視化する、商業用の開発プラットフォームです。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

5回答

9672閲覧

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

torano

総合スコア92

Unity3D

Unity3Dは、ゲームや対話式の3Dアプリケーション、トレーニングシュミレーション、そして医学的・建築学的な技術を可視化する、商業用の開発プラットフォームです。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

0グッド

0クリップ

投稿2019/04/10 15:55

編集2019/04/15 00:52

やりたいこと

動的に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ちゃんの服やリボンなどに変な模様が...
イメージ説明
テクスチャには変な模様はないのですが、モデルをみるとついています。

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答5

0

#追記

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

色漏れ出し軽減案

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

ShaderLab

1Shader "UnitychanPaint/Brush" 2{ 3 Properties 4 { 5 [HideInInspector] _BrushColor ("Brush Color", Color) = (1, 1, 1, 1) 6 [HideInInspector] _BrushTex ("Brush Texture", 2D) = "white" {} 7 [HideInInspector] _DepthTex ("Depth Texture", 2D) = "white" {} 8 } 9 10 SubShader 11 { 12 Tags { "RenderType" = "Opaque" } 13 14 Pass 15 { 16 Lighting Off 17 Cull Off 18 ZWrite Off 19 ZTest Always 20 Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha 21 22 // ステンシルを追加 23 Stencil 24 { 25 Ref 1 26 ReadMask 1 27 WriteMask 1 28 Comp Greater 29 Pass Replace 30 } 31 32 CGPROGRAM 33 34 #pragma vertex vert 35 #pragma geometry geom // geomを追加 36 #pragma fragment frag 37 38 #include "UnityCG.cginc" 39 40 struct appdata 41 { 42 float4 vertex: POSITION; 43 float2 uv4: TEXCOORD3; 44 }; 45 46 struct v2f 47 { 48 float4 brushUV: TEXCOORD0; 49 float4 vertex: SV_POSITION; 50 }; 51 52 float4 _BrushColor; 53 sampler2D _BrushTex; 54 sampler2D _DepthTex; 55 float4x4 _BrushVPMat; 56 float2 _VertexOffset; // オフセット量を追加 57 58 v2f vert(appdata v) 59 { 60 v2f o; 61 62 float2 position = v.uv4 * 2.0 - 1.0; 63 #if UNITY_UV_STARTS_AT_TOP 64 position.y *= -1.0; 65 #endif 66 o.vertex = float4(position, 0.0, 1.0); 67 68 float4x4 mvpMat = mul(_BrushVPMat, unity_ObjectToWorld); 69 o.brushUV = ComputeGrabScreenPos(mul(mvpMat, v.vertex)); 70 return o; 71 } 72 73 [maxvertexcount(3)] 74 void geom(triangle v2f i[3], inout TriangleStream<v2f> oStream) 75 { 76 // 3頂点を入力として受け取り、それぞれ重心から遠ざかる向きに_VertexOffsetに応じた量だけずらしています 77 // ポリゴンの角が鋭角なほど大きく引っ張るようにして、エッジの太り方が一様になるようにしました 78 // 頂点部分をあまり長く引っ張り出すと、他のポリゴンから色を漏れ出させるべき領域を侵食してしまうリスクが 79 // 大きくなるでしょうから、3頂点ではなく6個くらい頂点を出力して、角の部分は面取りしてやった方が 80 // 望ましいかもしれません 81 float2 p0 = i[0].vertex.xy; 82 float2 p1 = i[1].vertex.xy; 83 float2 p2 = i[2].vertex.xy; 84 float2 d01 = normalize(p1 - p0); 85 float2 d12 = normalize(p2 - p1); 86 float2 d20 = normalize(p0 - p2); 87 float2 amount0 = _VertexOffset / max(_VertexOffset, sqrt((1.0 - dot(d01, -d20)) * 0.5)); 88 float2 amount1 = _VertexOffset / max(_VertexOffset, sqrt((1.0 - dot(d12, -d01)) * 0.5)); 89 float2 amount2 = _VertexOffset / max(_VertexOffset, sqrt((1.0 - dot(d20, -d12)) * 0.5)); 90 float2 center = (p0 + p1 + p2) / 3.0; 91 v2f o; 92 o.vertex = float4(p0 + normalize(p0 - center) * amount0, 0.0, 1.0); 93 o.brushUV = i[0].brushUV; 94 oStream.Append(o); 95 o.vertex = float4(p1 + normalize(p1 - center) * amount1, 0.0, 1.0); 96 o.brushUV = i[1].brushUV; 97 oStream.Append(o); 98 o.vertex = float4(p2 + normalize(p2 - center) * amount2, 0.0, 1.0); 99 o.brushUV = i[2].brushUV; 100 oStream.Append(o); 101 } 102 103 fixed4 frag(v2f i): SV_Target 104 { 105 #ifdef UNITY_REVERSED_Z 106 float inverseZ = 1.0; 107 #else 108 float inverseZ = 0.0; 109 #endif 110 111 float brushAlpha = 0.0; 112 float depth = 1.0 - inverseZ; 113 114 if (i.brushUV.w > 0.0) 115 { 116 i.brushUV /= i.brushUV.w; 117 118 if (i.brushUV.x >= 0 && i.brushUV.x <= 1 && i.brushUV.y >= 0 && i.brushUV.y <= 1) 119 { 120 brushAlpha = tex2D(_BrushTex, i.brushUV).a; 121 depth = tex2D(_DepthTex, i.brushUV).r; 122 } 123 } 124 125 float near = UNITY_NEAR_CLIP_VALUE; 126 float far = 1.0 - inverseZ; 127 float normalizedZ = (i.brushUV.z - near) / (far - near); 128 129 #ifdef UNITY_REVERSED_Z 130 float normalizedDepth = 1 - depth; 131 #else 132 float normalizedDepth = depth; 133 #endif 134 135 float avoidZFighting = 0.001; 136 clip(normalizedDepth - normalizedZ + avoidZFighting); 137 138 return fixed4(_BrushColor.rgb, _BrushColor.a * brushAlpha); 139 } 140 ENDCG 141 142 } 143 } 144}

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

C#

1using System.Collections.Generic; 2using System.Linq; 3using UnityEngine; 4using UnityEngine.Rendering; 5 6namespace UnitychanPaint 7{ 8 public class Paintee : MonoBehaviour 9 { 10 private static readonly int PaintTexId = Shader.PropertyToID("_PaintTex"); 11 private static readonly int VertexOffsetId = Shader.PropertyToID("_VertexOffset"); // プロパティID追加 12 13 [SerializeField] private Vector2Int paintTextureSize = new Vector2Int(2048, 2048); 14 [SerializeField] private RenderTexture paintRt; 15 16 private CommandBuffer renderToTextureCommand; 17 private CommandBuffer renderToScreenCommand; 18 private IEnumerable<Renderer> renderers; 19 private Brush brush; 20 21 // 省略 22 23 private void InitTextures() 24 { 25 this.paintRt = new RenderTexture( 26 this.paintTextureSize.x, 27 this.paintTextureSize.y, 28 24, // 24ビットデプスにするとステンシルが使用可能になる 29 RenderTextureFormat.ARGB32, 30 RenderTextureReadWrite.Default) {name = "Paint"}; 31 this.paintRt.Create(); 32 } 33 34 // 省略 35 36 private void UpdateCommands(Brush b) 37 { 38 var brushMat = b.BrushMaterial; 39 var paintMat = b.PaintMaterial; 40 var r2t = this.renderToTextureCommand; 41 var r2s = this.renderToScreenCommand; 42 var unitVertexOffset = Vector2.one / this.paintTextureSize; 43 r2t.Clear(); 44 r2t.ClearRenderTarget(true, false, Color.clear); // ステンシルをクリア 45 r2s.Clear(); 46 r2s.SetGlobalTexture(PaintTexId, this.paintRt); 47 48 for (var j = 0; j < 2; j++) 49 { 50 // r2tにおいて、ポリゴン膨張なし塗り→3テクセル膨張塗りの2回塗りを行うように変更 51 // 色漏れ出し幅を広くする場合、一度に膨張させる幅を増やすよりも反復回数を増やした方が 52 // 描画コストはかかるものの、きれいな仕上がりになると思います 53 r2t.SetGlobalVector(VertexOffsetId, unitVertexOffset * (j * 3)); 54 55 foreach (var r in this.renderers) 56 { 57 // 手抜きして元のコードを残したため、2反復の中で毎回subMeshCountを求めています... 58 var subMeshCount = 0; 59 switch (r) 60 { 61 case MeshRenderer mr: 62 subMeshCount = mr.GetComponent<MeshFilter>().sharedMesh.subMeshCount; 63 break; 64 case SkinnedMeshRenderer smr: 65 subMeshCount = smr.sharedMesh.subMeshCount; 66 break; 67 default: 68 subMeshCount = 1; 69 break; 70 } 71 72 for (var i = 0; i < subMeshCount; i++) 73 { 74 r2t.DrawRenderer(r, brushMat, i); 75 } 76 77 if (j > 0) 78 { 79 continue; 80 } 81 82 for (var i = 0; i < subMeshCount; i++) 83 { 84 r2s.DrawRenderer(r, paintMat, i); 85 } 86 } 87 } 88 } 89 90 // 省略 91 } 92}

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

膨張なし
膨張なし

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

投稿2019/04/27 13:38

Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

torano

2019/04/29 14:50

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

0

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

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

C#

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

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

UV、UV2

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

テクスチャ再配置

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

ShaderLab

1Shader "Unlit/UVToUV2" 2{ 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 } 7 SubShader 8 { 9 Tags { "RenderType"="Opaque" } 10 11 Pass 12 { 13 Cull Off 14 ZWrite Off 15 ZTest Always 16 17 CGPROGRAM 18 #pragma vertex vert 19 #pragma fragment frag 20 #include "UnityCG.cginc" 21 22 struct appdata 23 { 24 float2 uv : TEXCOORD0; 25 float2 uv2 : TEXCOORD1; 26 }; 27 28 struct v2f 29 { 30 float2 uv : TEXCOORD0; 31 float4 vertex : SV_POSITION; 32 }; 33 34 sampler2D _MainTex; 35 float4 _MainTex_ST; 36 37 v2f vert(appdata v) 38 { 39 v2f o; 40 41 // 画面上の頂点の位置はUV2をもとに配置し... 42 float2 position = v.uv2 * 2.0 - 1.0; 43 #if UNITY_UV_STARTS_AT_TOP 44 position.y *= -1.0; 45 #endif 46 o.vertex = float4(position, 0.0, 1.0); 47 48 // テクスチャはUVの位置からサンプリングする 49 o.uv = TRANSFORM_TEX(v.uv, _MainTex); 50 51 return o; 52 } 53 54 fixed4 frag(v2f i) : SV_Target 55 { 56 fixed4 col = tex2D(_MainTex, i.uv); 57 return col; 58 } 59 ENDCG 60 } 61 } 62}

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

C#

1// 単純なBlitの代わりに... 2// Graphics.Blit(mainTex, modelMainRT); 3 4// テクスチャの各部位をUV2に合わせた位置に再配置しながらコピーする 5var remapper = new Material(Shader.Find("Unlit/UVToUV2")); 6remapper.mainTexture = mainTex; 7var c = new CommandBuffer {name = "Remap modelMainTex"}; 8c.SetRenderTarget(modelMainRT); 9c.DrawRenderer(renderer, remapper); 10Graphics.ExecuteCommandBuffer(c);

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

結果

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

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

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

投稿2019/04/11 21:04

Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

torano

2019/04/13 00:31

いつもいつもありがとうございます。 なるほど、UV2用専用の、重なりなしのテクスチャを動的につくってやればいいのですね。モデルのマテリアルではuvを使うだろうと思うので、C#側でもしlightmap uvを使用しているならばremapper用シェーダーを適用後にmeshのuvをuv2に置き換えてやってみましたらうまくいきました。ただし、おっしゃるようにテクスチャの劣化がみられました。黒い線が目にみえてわかるようにでてきてしまいます。他の部分の劣化はそこまでひどくないような気がしますが、これはちょっと許容できないですね。。。 もともと重なりが前提のテクスチャを重なりなしのものにするということで、テクスチャのサイズが大きくなるのは仕方ないと思い、とりあえずwidth, height両方2倍してやってみましたが、黒い線はあいかわらず。。。これはremapperマテリアルでUV2用のテクスチャをつくったときに発生する誤差、ということなんでしょうか。 最後にご提案された方法ですが、ペイントは別のUV2用のものにし、モデルのマテリアルのサーフェスシェーダー(またはフラグメントシェーダー?)の段階で、モデルのメインテクスチャからはuvを使い、ペイント専用のテクスチャからはuv2を使って色をとってきて、ペイントテクスチャの方のアルファをもとに、色をブレンドする、ということですよね。確かにこれならば実現できそうなのですが、できればモデルのマテリアルを変えたくはないのです。というのも、難しい話だとは思いますが、なるべく汎用的なペイントアプリにしたい=どんなマテリアルのモデルをもインポートしてペイントしたい、と思ってるからです。(そもそもモデルが自由ならばモデル作成の段階でUVの重なりがないようにしてしまえばいいので。)また、途中はともかく、最終的にはペイントされたテクスチャを保存したいので結局UV2のテクスチャを使用することになると思います。 とはいえ、劣化はどうしようもないので、劣化するかテクスチャの解像度をあげるかは妥協しようと思います。黒い線だけはなんとかしたいですが。。。 もう一つ黒い線の解決方法として挙げられたジオメトリシェーダーですが、使ったことがないのでよくわからないのですがこの週末にちょっと調べながらやってみようと思います。
Bongo

2019/04/13 03:46

なるほど、モデルのマテリアルには手を付けたくないわけですね。汎用的にしたいという理由を抜きにしても、確かにユニティちゃんのようないくつもマテリアルを持っているようなモデルについて、シェーダーを一つ一つ改造するというのはやりたくないですね... 私としても興味深いテーマですので、ぜひ検討してみようと思います。もし何かいい手が思いつきましたら追記します。
torano

2019/04/15 00:46

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

2019/04/15 01:06

ジオメトリーシェーダーは一案として口走っただけですので、他の方法でも問題ないでしょう。どうやらいい感じにポリゴン境界を埋めることができているようですので、このやり方でいいと思います。 変な模様の件ですが、パターンが腰回りのヒラヒラ(スカート?)に似ていますね。他にも、袖の内側にジャケット側面の市松模様らしきパターンが現れているようです。 もしかして、法線マップもUV2でサンプリングしてしまってはいないでしょうか?
torano

2019/04/15 01:43

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

0

ベストアンサー

  • MouseOperatorの続き

C#

1 public class PreprocessUpdateInfoObservable : OperatorObservableBase<MouseOperator.UpdateInfo> 2 { 3 private readonly IObservable<MouseOperator.UpdateInfo> source; 4 5 public PreprocessUpdateInfoObservable(IObservable<MouseOperator.UpdateInfo> source) : base( 6 source.IsRequiredSubscribeOnCurrentThread()) 7 { 8 this.source = source; 9 } 10 11 protected override IDisposable SubscribeCore(IObserver<MouseOperator.UpdateInfo> observer, IDisposable cancel) 12 { 13 return new PreprocessUpdateInfo(this, observer, cancel).Run(); 14 } 15 16 // count only 17 private class PreprocessUpdateInfo : OperatorObserverBase<MouseOperator.UpdateInfo, MouseOperator.UpdateInfo> 18 { 19 private bool initialized; 20 private readonly PreprocessUpdateInfoObservable parent; 21 private MouseOperator.UpdateInfo prev; 22 23 public PreprocessUpdateInfo( 24 PreprocessUpdateInfoObservable parent, 25 IObserver<MouseOperator.UpdateInfo> observer, 26 IDisposable cancel) : base(observer, cancel) 27 { 28 this.parent = parent; 29 } 30 31 public override void OnCompleted() 32 { 33 try 34 { 35 this.observer?.OnCompleted(); 36 } 37 finally 38 { 39 this.Dispose(); 40 } 41 } 42 43 public override void OnError(Exception error) 44 { 45 try 46 { 47 this.observer?.OnError(error); 48 } 49 finally 50 { 51 this.Dispose(); 52 } 53 } 54 55 public override void OnNext(MouseOperator.UpdateInfo value) 56 { 57 if (!this.initialized) 58 { 59 this.prev = value; 60 this.initialized = true; 61 } 62 else 63 { 64 var p = this.prev; 65 value.MouseButtonDown = !p.MouseButton && value.MouseButton; 66 value.MouseButtonUp = p.MouseButton && !value.MouseButton; 67 value.SpaceDown = !p.Space && value.Space; 68 value.SpaceUp = p.Space && !value.Space; 69 value.ShiftDown = !p.Shift && value.Shift; 70 value.ShiftUp = p.Shift && !value.Shift; 71 value.PointerOverUiEnter = !p.IsPointerOverUi && value.IsPointerOverUi; 72 value.PointerOverUiExit = p.IsPointerOverUi && !value.IsPointerOverUi; 73 this.prev = value; 74 this.observer?.OnNext(value); 75 } 76 } 77 78 public IDisposable Run() 79 { 80 return this.parent.source.Subscribe(this); 81 } 82 } 83 } 84 85 public static class IObservableExtensions 86 { 87 public static IObservable<MouseOperator.UpdateInfo> PreprocessUpdateInfo( 88 this IObservable<MouseOperator.UpdateInfo> source) 89 { 90 if (source == null) 91 { 92 throw new ArgumentNullException(nameof(source)); 93 } 94 95 return new PreprocessUpdateInfoObservable(source); 96 } 97 } 98}

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

ヒエラルキー

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

ShaderLab

1Shader "UnitychanPaint/Brush" 2{ 3 Properties 4 { 5 [HideInInspector] _BrushColor ("Brush Color", Color) = (1, 1, 1, 1) 6 [HideInInspector] _BrushTex ("Brush Texture", 2D) = "white" {} 7 [HideInInspector] _DepthTex ("Depth Texture", 2D) = "white" {} 8 } 9 10 SubShader 11 { 12 Tags { "RenderType" = "Opaque" } 13 14 Pass 15 { 16 Lighting Off 17 Cull Off 18 ZWrite Off 19 ZTest Always 20 Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha 21 22 CGPROGRAM 23 24 #pragma vertex vert 25 #pragma fragment frag 26 27 #include "UnityCG.cginc" 28 29 struct appdata 30 { 31 float4 vertex: POSITION; 32 float2 uv4: TEXCOORD3; 33 }; 34 35 struct v2f 36 { 37 float4 brushUV: TEXCOORD0; 38 float4 vertex: SV_POSITION; 39 }; 40 41 float4 _BrushColor; 42 sampler2D _BrushTex; 43 sampler2D _DepthTex; 44 float4x4 _BrushVPMat; 45 46 v2f vert(appdata v) 47 { 48 v2f o; 49 50 float2 position = v.uv4 * 2.0 - 1.0; 51 #if UNITY_UV_STARTS_AT_TOP 52 position.y *= -1.0; 53 #endif 54 o.vertex = float4(position, 0.0, 1.0); 55 56 float4x4 mvpMat = mul(_BrushVPMat, unity_ObjectToWorld); 57 o.brushUV = ComputeGrabScreenPos(mul(mvpMat, v.vertex)); 58 return o; 59 } 60 61 fixed4 frag(v2f i): SV_Target 62 { 63 #ifdef UNITY_REVERSED_Z 64 float inverseZ = 1.0; 65 #else 66 float inverseZ = 0.0; 67 #endif 68 69 float brushAlpha = 0.0; 70 float depth = 1.0 - inverseZ; 71 72 if (i.brushUV.w > 0.0) 73 { 74 i.brushUV /= i.brushUV.w; 75 76 if (i.brushUV.x >= 0 && i.brushUV.x <= 1 && i.brushUV.y >= 0 && i.brushUV.y <= 1) 77 { 78 brushAlpha = tex2D(_BrushTex, i.brushUV).a; 79 depth = tex2D(_DepthTex, i.brushUV).r; 80 } 81 } 82 83 float near = UNITY_NEAR_CLIP_VALUE; 84 float far = 1.0 - inverseZ; 85 float normalizedZ = (i.brushUV.z - near) / (far - near); 86 87 #ifdef UNITY_REVERSED_Z 88 float normalizedDepth = 1 - depth; 89 #else 90 float normalizedDepth = depth; 91 #endif 92 93 float avoidZFighting = 0.001; 94 clip(normalizedDepth - normalizedZ + avoidZFighting); 95 96 return fixed4(_BrushColor.rgb, _BrushColor.a * brushAlpha); 97 } 98 ENDCG 99 100 } 101 } 102}

ShaderLab

1Shader "UnitychanPaint/Paint" 2{ 3 Properties 4 { 5 _Glossiness ("Smoothness", Range(0.0, 1.0)) = 0.85 6 _Metallic ("Metallic", Range(0.0, 1.0)) = 0.0 7 _AmbientBoost ("Ambient Boost", Range(0.0, 1.0)) = 0.5 8 [HideInInspector] _BrushColor ("Brush Color", Color) = (1, 1, 1, 1) 9 [HideInInspector] _BrushTex ("Brush Texture", 2D) = "white" {} 10 [HideInInspector] _DepthTex ("Depth Texture", 2D) = "white" {} 11 [HideInInspector] _DrawBrushSpot ("Draw Brush Spot", Float) = 1.0 12 } 13 SubShader 14 { 15 Tags { "RenderType"="Transparent" "Queue"="Transparent" } 16 17 Cull Back 18 ZWrite Off 19 ZTest LEqual 20 21 CGPROGRAM 22 #pragma surface surf Paint vertex:vert decal:blend exclude_path:deferred exclude_path:prepass nometa 23 #pragma target 3.0 24 #include "UnityPBSLighting.cginc" 25 26 sampler2D _PaintTex; 27 float4 _BrushColor; 28 sampler2D _BrushTex; 29 sampler2D _DepthTex; 30 float4x4 _BrushVPMat; 31 fixed _DrawBrushSpot; 32 half _Glossiness; 33 half _Metallic; 34 half _AmbientBoost; 35 36 struct Input 37 { 38 float2 uv4_PaintTex; 39 float4 brushUV; 40 }; 41 42 inline half4 LightingPaint(SurfaceOutputStandard s, half3 viewDir, UnityGI gi) 43 { 44 return LightingStandard(s, viewDir, gi); 45 } 46 47 inline void LightingPaint_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi) 48 { 49 LightingStandard_GI(s, data, gi); 50 51 // 通常のレンダリング時と違って環境光が効かないらしく、陰の部分が真っ暗になってしまうようでした 52 // そこで、環境光強度を無理やり増やして見た目を調節できるようにしました 53 gi.indirect.diffuse += _AmbientBoost; 54 } 55 56 void vert(inout appdata_full v, out Input o) { 57 UNITY_INITIALIZE_OUTPUT(Input,o); 58 float4x4 mvpMat = mul(_BrushVPMat, unity_ObjectToWorld); 59 o.brushUV = ComputeGrabScreenPos(mul(mvpMat, v.vertex)); 60 } 61 62 void surf(Input IN, inout SurfaceOutputStandard o) 63 { 64 #ifdef UNITY_REVERSED_Z 65 float inverseZ = 1.0; 66 #else 67 float inverseZ = 0.0; 68 #endif 69 70 float brushAlpha = 0.0; 71 float depth = 1.0 - inverseZ; 72 73 if (IN.brushUV.w > 0.0) 74 { 75 IN.brushUV /= IN.brushUV.w; 76 77 if (IN.brushUV.x >= 0 && IN.brushUV.x <= 1 && IN.brushUV.y >= 0 && IN.brushUV.y <= 1) 78 { 79 brushAlpha = tex2D(_BrushTex, IN.brushUV).a; 80 depth = tex2D(_DepthTex, IN.brushUV).r; 81 } 82 } 83 84 float near = UNITY_NEAR_CLIP_VALUE; 85 float far = 1.0 - inverseZ; 86 float normalizedZ = (IN.brushUV.z - near) / (far - near); 87 88 #ifdef UNITY_REVERSED_Z 89 float normalizedDepth = 1 - depth; 90 #else 91 float normalizedDepth = depth; 92 #endif 93 94 float avoidZFighting = 0.001; 95 float depthClipping = step(0.0, normalizedDepth - normalizedZ + avoidZFighting); 96 97 float4 brush = float4(_BrushColor.rgb, _BrushColor.a * brushAlpha * depthClipping * _DrawBrushSpot); 98 float4 paint = tex2D(_PaintTex, IN.uv4_PaintTex); 99 paint.rgb /= max(paint.a, 0.001); 100 float3 color = lerp(paint.rgb, brush.rgb, brush.a); 101 float alpha = brush.a + paint.a * (1.0 - brush.a); 102 103 #ifndef UNITY_COLORSPACE_GAMMA 104 color = GammaToLinearSpace(color); 105 #endif 106 107 o.Albedo = color; 108 o.Metallic = _Metallic; 109 o.Smoothness = _Glossiness; 110 o.Alpha = alpha; 111 } 112 ENDCG 113 } 114 FallBack "Diffuse" 115}

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

結果1

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

結果2

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

投稿2019/04/16 21:39

Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

torano

2019/04/26 10:58 編集

返信だいぶ遅くなりすみません。自分でシーン試して実行してみたのですが、うまくいきました。実装方法ですが以下の認識であっているでしょうか? 塗り専用のレンダーテクスチャをテクスチャアトラスの要領で全体にひとつだけ作成する。これはUV2でのテクスチャである。MouseOperatorでマウス左クリックが押された時Brushシェーダーを使ってこのテクスチャに塗りこみを行う。 モデルの描画は、通常のモデルのレンダリングが終了したあと、Paintシェーダーを使って上塗りする。これはsurfシェーダーで、ライティングが適用される。上塗り時には、塗り用のレンダーテクスチャからUV2(UV4)をつかってペイント部分の色をとってくる。左クリックしてない間もプレビューとしてマウスカーソルのあたりにブラシの形と色が表示されるが、これもこのシェーダー内で行われている。 いろいろと参考になる部分があったのですが、特にテクスチャアトラスの部分は他の実装でも応用できそうな気がしました。この実装だとそれぞれのテクスチャについて黒い線ができないようにうまい具合にUV2専用テクスチャを作成する、なんて複雑なことしなくても済みますね。 しかし、この実装だと編集最中はスムーズにいきますが、最後の出力部分が大変そうです。単純にテクスチャに書き込んでしまうと、編集時はPaintシェーダーでどのようにレンダリングされるか決まっていたのが、出力したらモデルのシェーダーになるので見た目が塗っている時と変わってしまった、となりそうです。というか、出力時にテクスチャに塗りこむならば結局UVをUV2にいれかえ、UV2用のテクスチャをそれぞれつくらなければなりません。なかなか難しいですね。最初からUV2用のを作成するか、後から作成するか、どちらにせようまいこと劣化しないように(劣化してないように見せるように)する方法を頑張って考えなければならなそうです。 とりあえず、最初に疑問だったUV2を使って重複しないように塗るという疑問は解決したので解決済みにしておきます。本当にありがとうございます。 ステンシルは最初黒線問題を考えているときにちらりと頭にうかんだのですが、うまい方法が思いつきませんでした。あれはレンダーされた場所、されてない場所を明らかにし、その結果を使うということですよね。しかし黒線が現れるレンダラーの境界がレンダーされてない場所になっているわけでもないと思うので難しいのかなと思いました。もし可能そうであればアイデアだけでも教えてほしいです。
Bongo

2019/04/27 13:39

最終的にペイント完了結果を元のテクスチャと統合して出力したい、しかもペイント対象のモデルを任意に選べるようにしたい、という条件があるのがなかなかやっかいですね。 3D画面上での表示だけがペイントされているように見えれば十分ならペイントだけ別テクスチャ方式がよさそうですし、逆にモデルのデザインに「UVの重なり・はみ出し禁止」と縛りを与えることができるならメインテクスチャへの直接描き込み方式が楽そうですし...悩ましい問題だと思います。 黒線問題についてですが、後で考え直してみたら「注目テクセルの周囲のテクセルの透明度を調べてエッジ拡張」方式とステンシルを組み合わせてもいまいちのような気がしてきました。やるとしたら近傍ピクセルのステンシル値を取得する必要がありそうですが、それは無理っぽいので、結局ステンシルの代替となるようなテクスチャをもう一枚用意することになりそうで面倒そうです。 以前に申し上げた「ジオメトリーシェーダーステージを追加してポリゴンを太らせる」の方がマシなように思われたので、ちょっと検討してみました。またもや文字数が限界に達してしまい別回答です。すみません...
guest

0

  • Brushの続き

C#

1 public void LookAt(Vector3 worldAimPosition) 2 { 3 var localAimPosition = this.parentTransform.InverseTransformPoint(worldAimPosition); 4 this.brushTransform.localRotation = Quaternion.LookRotation(localAimPosition); 5 } 6 7 private void Awake() 8 { 9 this.BrushCamera = this.GetComponent<Camera>(); 10 this.BrushCamera.aspect = 1; 11 this.BrushCamera.enabled = false; 12 this.BrushCamera.orthographic = false; 13 this.BrushCamera.renderingPath = RenderingPath.VertexLit; 14 this.BrushCamera.clearFlags = CameraClearFlags.Depth; 15 this.BrushCamera.depthTextureMode = DepthTextureMode.Depth; 16 this.BrushCamera.allowHDR = false; 17 this.BrushCamera.allowMSAA = false; 18 this.BrushCamera.allowDynamicResolution = false; 19 20 this.brushTransform = this.transform; 21 this.parentTransform = this.brushTransform.parent; 22 this.brushTransform.localPosition = Vector3.zero; 23 this.brushTransform.localRotation = Quaternion.identity; 24 25 this.InitTextures(); 26 this.InitMaterials(); 27 } 28 29 private void Start() 30 { 31 if (this.target != null) 32 { 33 this.Target = this.target; 34 } 35 } 36 37 private void InitTextures() 38 { 39 var depthBufferWidth = this.BrushShape.width; 40 var depthBufferHeight = this.BrushShape.height; 41 this.colorRt = new RenderTexture( 42 depthBufferWidth, 43 depthBufferHeight, 44 0, 45 RenderTextureFormat.ARGB32) {name = "BrushColor"}; 46 this.colorRt.Create(); 47 this.depthRt = new RenderTexture( 48 depthBufferWidth, 49 depthBufferHeight, 50 24, 51 RenderTextureFormat.Depth) {name = "BrushDepth"}; 52 this.depthRt.Create(); 53 this.BrushCamera.SetTargetBuffers(this.colorRt.colorBuffer, this.depthRt.depthBuffer); 54 } 55 56 private void InitMaterials() 57 { 58 this.brushMaterial.SetColor(BrushColorId, this.BrushColor); 59 this.brushMaterial.SetTexture(BrushTexId, this.BrushShape); 60 this.brushMaterial.SetTexture(DepthTexId, this.depthRt); 61 this.paintMaterial.SetColor(BrushColorId, this.BrushColor); 62 this.paintMaterial.SetTexture(BrushTexId, this.BrushShape); 63 this.paintMaterial.SetTexture(DepthTexId, this.depthRt); 64 } 65 66 private void OnDestroy() 67 { 68 this.ReleaseTextures(); 69 } 70 71 private void ReleaseTextures() 72 { 73 this.colorRt.Release(); 74 this.depthRt.Release(); 75 } 76 } 77}
  • MouseOperator

本題のペイント機構には直接関係ありませんが、このスクリプトはヒエラルキーのルートにある単独のオブジェクトにアタッチされており、マウス操作に応じて視点やブラシを操作する作業を担当します。ボタン類の押下状態に応じて分岐する部分が整理されておらず、ごちゃごちゃしていますがご容赦ください...

C#

1using System; 2using System.Linq; 3using UniRx; 4using UniRx.Operators; 5using UniRx.Triggers; 6using UnityEngine; 7using UnityEngine.EventSystems; 8 9namespace UnitychanPaint 10{ 11 public class MouseOperator : MonoBehaviour 12 { 13 [SerializeField] private GameObject target; 14 [SerializeField] private CanvasGroup uiCanvases; 15 [SerializeField] private Brush brush; 16 [SerializeField] private Texture2D textureCross; 17 [SerializeField] private Texture2D textureGrabClosed; 18 [SerializeField] private Texture2D textureGrabOpen; 19 [SerializeField] private Vector2 hotSpot; 20 [SerializeField] private float factorRotation; 21 [SerializeField] private float factorZoom; 22 private new Camera camera; 23 private Transform cameraTransform; 24 private Vector3 focus; 25 private Vector3 previousMouseWorldPosition; 26 27 private void Start() 28 { 29 if (Camera.main != null) 30 { 31 this.camera = Camera.main; 32 this.cameraTransform = this.camera.transform; 33 } 34 35 Cursor.SetCursor(this.textureCross, this.hotSpot, CursorMode.ForceSoftware); 36 37 // 初期注目点として、ターゲットのレンダラーのバウンディングボックスをすべて結合したボックスの中心点を選ぶ 38 var t = this.target; 39 if (t != null) 40 { 41 var renderers = t.GetComponentsInChildren<Renderer>(); 42 if (t.transform != null) 43 { 44 this.focus = (renderers == null) || !renderers.Any() 45 ? t.transform.position 46 : renderers.Where(r => r != null).Select(r => r.bounds).Aggregate( 47 (u, b) => 48 { 49 u.Encapsulate(b); 50 return u; 51 }).center; 52 } 53 } 54 55 this.UpdateAsObservable().Select( 56 _ => 57 { 58 var cam = this.camera; 59 var camTrans = this.cameraTransform; 60 var eveSys = EventSystem.current; 61 if ((cam == null) || (camTrans == null) || (eveSys == null)) 62 { 63 throw new InvalidOperationException( 64 $"{nameof(cam)}:{cam} {nameof(camTrans)}:{camTrans} {nameof(eveSys)}:{eveSys}"); 65 } 66 67 // 毎フレームマウスポインタの指す方角やマウスボタン・キーの押下状態を取得する 68 // さらに後段のPreprocessUpdateInfoで「UIにポインタが入った/出た」「キーが押された/離された」といった 69 // 状態変化を検出する 70 return new UpdateInfo 71 { 72 Camera = cam, CameraTransform = camTrans, 73 IsPointerOverUi = eveSys.IsPointerOverGameObject(), 74 MouseRay = cam.ScreenPointToRay(Input.mousePosition), 75 Shift = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift), 76 Space = Input.GetKey(KeyCode.Space), 77 MouseButton = Input.GetMouseButton(0) 78 }; 79 }).PreprocessUpdateInfo().Subscribe( 80 info => 81 { 82 var camTransform = info.CameraTransform; 83 var camTransformPosition = camTransform.position; 84 var camTransformForward = camTransform.forward; 85 86 // マウスホイールでメインカメラを前後に移動 87 camTransformPosition += camTransformForward * Input.mouseScrollDelta.y * this.factorZoom; 88 camTransform.position = camTransformPosition; 89 90 // メインカメラを注目点に向ける 91 // カメラは傾いてしまいますが、ドラッグ視点操作に対する一貫性があるような気がして 92 // LookAtではなくFromToRotationを使用しました 93 camTransform.rotation = 94 Quaternion.FromToRotation(camTransformForward, this.focus - camTransformPosition) * 95 camTransform.rotation; 96 97 // 視点操作中はUIを半透明にし、使用不能にする 98 var uis = this.uiCanvases; 99 if (uis != null) 100 { 101 if (info.SpaceDown) 102 { 103 uis.interactable = false; 104 uis.alpha = 0.5f; 105 } 106 else if (info.SpaceUp) 107 { 108 uis.interactable = true; 109 uis.alpha = 1.0f; 110 } 111 } 112 113 // キーの状態やポインタがUI上にあるかどうかでマウスポインタのグラフィックを変更する 114 if (info.MouseButtonDown || info.MouseButtonUp || info.PointerOverUiEnter || 115 info.PointerOverUiExit || info.SpaceDown || info.SpaceUp) 116 { 117 if (info.IsPointerOverUi && !info.Space) 118 { 119 Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto); 120 } 121 else 122 { 123 Cursor.SetCursor( 124 !info.Space ? this.textureCross : 125 info.MouseButton ? this.textureGrabClosed : this.textureGrabOpen, 126 this.hotSpot, 127 CursorMode.Auto); 128 } 129 130 this.brush.NeedsDrawUnpaintedBrushSpot = !info.Space; 131 } 132 133 // 現在マウスポインタが指しているワールド空間内の点を調べる 134 var forward = camTransform.forward; 135 var focusPlane = new Plane(-forward, this.focus); 136 focusPlane.Raycast(info.MouseRay, out var enter); 137 var mouseWorldPosition = info.MouseRay.GetPoint(enter); 138 if (info.MouseButtonDown) 139 { 140 this.previousMouseWorldPosition = mouseWorldPosition; 141 } 142 143 // スペースキーを押しながらドラッグで視点を回転、さらにシフトキー同時押しで 144 // 注視点とカメラを平行移動する 145 if (info.Space && info.MouseButton) 146 { 147 var mouseDelta = (mouseWorldPosition - this.previousMouseWorldPosition) / enter; 148 this.previousMouseWorldPosition = mouseWorldPosition; 149 if (info.Shift) 150 { 151 this.focus -= mouseDelta; 152 this.previousMouseWorldPosition -= mouseDelta; 153 camTransform.Translate(-mouseDelta, Space.World); 154 } 155 else 156 { 157 var rotationAxis = Vector3.Cross(forward, mouseDelta).normalized; 158 var rotationAngle = mouseDelta.magnitude * this.factorRotation; 159 camTransform.RotateAround(this.focus, rotationAxis, rotationAngle); 160 } 161 162 forward = camTransform.forward; 163 focusPlane = new Plane(-forward, this.focus); 164 focusPlane.Raycast(info.MouseRay, out enter); 165 mouseWorldPosition = info.MouseRay.GetPoint(enter); 166 } 167 168 // ブラシをマウスポインタの方角に向け、マウスボタンが押されていれば 169 // Painteeに対してペイントテクスチャへブラシを書き込むよう指示する 170 this.brush.LookAt(mouseWorldPosition); 171 if (!info.Space && !info.IsPointerOverUi && info.MouseButton) 172 { 173 var model = this.brush.Target; 174 if (model != null) 175 { 176 model.RenderToTexture(); 177 } 178 } 179 }).AddTo(this.gameObject); 180 181 // 色が切り替えられたらブラシの色を変更 182 var colorSelector = FindObjectOfType<ColorSelector>(); 183 if (colorSelector != null) 184 { 185 colorSelector.color.Subscribe(color => { this.brush.BrushColor = color; }); 186 } 187 188 // サイズが変えられたらブラシカメラの画角を変更 189 var sizeSelector = FindObjectOfType<SizeSelector>(); 190 if (sizeSelector != null) 191 { 192 sizeSelector.fieldOfView.Subscribe(fov => this.brush.BrushCamera.fieldOfView = fov); 193 } 194 } 195 196 public struct UpdateInfo 197 { 198 public Camera Camera; 199 public Transform CameraTransform; 200 public Ray MouseRay; 201 public bool Space; 202 public bool SpaceDown; 203 public bool SpaceUp; 204 public bool Shift; 205 public bool ShiftDown; 206 public bool ShiftUp; 207 public bool IsPointerOverUi; 208 public bool PointerOverUiEnter; 209 public bool PointerOverUiExit; 210 public bool MouseButton; 211 public bool MouseButtonDown; 212 public bool MouseButtonUp; 213 } 214 }

投稿2019/04/16 21:37

Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

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

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

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

  • Paintee

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

C#

1using System.Collections.Generic; 2using System.Linq; 3using UnityEngine; 4using UnityEngine.Rendering; 5 6namespace UnitychanPaint 7{ 8 public class Paintee : MonoBehaviour 9 { 10 private static readonly int PaintTexId = Shader.PropertyToID("_PaintTex"); 11 12 [SerializeField] private Vector2Int paintTextureSize = new Vector2Int(2048, 2048); 13 [SerializeField] private RenderTexture paintRt; 14 15 private CommandBuffer renderToTextureCommand; 16 private CommandBuffer renderToScreenCommand; 17 private IEnumerable<Renderer> renderers; 18 private Brush brush; 19 20 public Brush Brush 21 { 22 get => this.brush; 23 set 24 { 25 this.brush = value; 26 if (value != null) 27 { 28 this.UpdateCommands(value); 29 } 30 } 31 } 32 33 public void RenderToTexture() 34 { 35 var b = this.Brush; 36 if (b == null) 37 { 38 return; 39 } 40 41 Graphics.SetRenderTarget(this.paintRt); 42 b.UpdateDepth(); 43 b.UpdateViewProjectionMatrix(b.BrushMaterial); 44 Graphics.ExecuteCommandBuffer(this.renderToTextureCommand); 45 } 46 47 private void RenderToScreen() 48 { 49 var b = this.Brush; 50 if (b == null) 51 { 52 return; 53 } 54 55 b.UpdateDepth(); 56 b.UpdateViewProjectionMatrix(b.PaintMaterial); 57 Graphics.ExecuteCommandBuffer(this.renderToScreenCommand); 58 } 59 60 private void Awake() 61 { 62 this.renderers = this.GetComponentsInChildren<Renderer>(); 63 this.InitModels(); 64 this.InitTextures(); 65 this.InitCommands(); 66 } 67 68 private void InitModels() 69 { 70 // 念のための処置としてオブジェクトの各レンダラー経由で全メッシュを取得、 71 // それらメッシュを複製してオリジナルは元のまま残す 72 // UV2には事前に面の重複がないようポリゴンが配置されており、また各メッシュの 73 // ポリゴン配置はそれぞれUV空間全面を使っていることを想定する 74 // (どうやら「Generate Lightmap UVs」で作ったUV2はこうなるようです 75 // もしUV2が適切に作られていないモデルにも対応したい場合、このメソッド中で 76 // さらに独自のUV展開処理を行うことになるでしょう) 77 // モデル全体で一枚のペイントテクスチャを使うことにしたため、複数のメッシュがあるならば 78 // 各メッシュのUV2を縮小してタイル上に並べ、全モデルで一枚のテクスチャを使っても 79 // 面の重なりが発生しないようにする 80 // また、「UVの働きを検証する – Tsumiki Tech Times|積木製作」(http://tsumikiseisaku.com/blog/how-uv-works/) に 81 // よると、UV2やUV3は後でUnity側からいじられる恐れがあるため、念のためできあがったUV2は 82 // UV4にセットして、ブラシ塗りや画面上の描画にもUV4を使う 83 var rs = this.renderers; 84 if (rs == null) 85 { 86 return; 87 } 88 89 var mrMeshPairs = rs.OfType<MeshRenderer>().Select(mr => (mr, mr.GetComponent<MeshFilter>().sharedMesh)) 90 .ToArray(); 91 var smrMeshPairs = rs.OfType<SkinnedMeshRenderer>().Select(smr => (smr, smr.sharedMesh)).ToArray(); 92 var meshToRenderer = new Dictionary<Mesh, (List<MeshRenderer>, List<SkinnedMeshRenderer>)>(); 93 foreach (var (r, m) in mrMeshPairs) 94 { 95 if (meshToRenderer.TryGetValue(m, out var element)) 96 { 97 element.Item1.Add(r); 98 } 99 else 100 { 101 meshToRenderer.Add(m, (new List<MeshRenderer> {r}, new List<SkinnedMeshRenderer>())); 102 } 103 } 104 105 foreach (var (r, m) in smrMeshPairs) 106 { 107 if (meshToRenderer.TryGetValue(m, out var element)) 108 { 109 element.Item2.Add(r); 110 } 111 else 112 { 113 meshToRenderer.Add(m, (new List<MeshRenderer>(), new List<SkinnedMeshRenderer> {r})); 114 } 115 } 116 117 var clones = new List<Mesh>(); 118 foreach (var element in meshToRenderer) 119 { 120 var clone = Instantiate(element.Key); 121 clones.Add(clone); 122 foreach (var meshRenderer in element.Value.Item1) 123 { 124 meshRenderer.GetComponent<MeshFilter>().sharedMesh = clone; 125 } 126 127 foreach (var skinnedMeshRenderer in element.Value.Item2) 128 { 129 skinnedMeshRenderer.sharedMesh = clone; 130 } 131 } 132 133 var meshCount = clones.Count; 134 if (meshCount == 0) 135 { 136 return; 137 } 138 139 var rows = 1; 140 var columns = 1; 141 while ((rows * columns) < meshCount) 142 { 143 columns++; 144 if ((rows * columns) >= meshCount) 145 { 146 break; 147 } 148 149 rows++; 150 } 151 152 Debug.Log($"Pack {meshCount} meshes into {rows} x {columns} atlas."); 153 var index = 0; 154 var uv2 = new List<Vector2>(); 155 var tileSize = new Vector2(1.0f / columns, 1.0f / rows); 156 for (var j = 0; j < rows; j++) 157 { 158 for (var i = 0; i < columns; i++) 159 { 160 var mesh = clones[index]; 161 mesh.GetUVs(1, uv2); 162 var uvCount = uv2.Count; 163 var offset = new Vector2(i, j); 164 for (var k = 0; k < uvCount; k++) 165 { 166 uv2[k] = (uv2[k] + offset) * tileSize; 167 } 168 mesh.SetUVs(3, uv2); 169 index++; 170 } 171 } 172 } 173 174 private void InitTextures() 175 { 176 this.paintRt = new RenderTexture( 177 this.paintTextureSize.x, 178 this.paintTextureSize.y, 179 0, 180 RenderTextureFormat.ARGB32, 181 RenderTextureReadWrite.Default) {name = "Paint"}; 182 this.paintRt.Create(); 183 } 184 185 private void InitCommands() 186 { 187 // ゲームプレイ中にブラシを別のブラシオブジェクトに切り替えることがあるかも 188 // しれないことを考慮し、初期化タイミングではコマンドバッファを生成するだけとして 189 // 内容の構成はUpdateCommandsに分離した 190 this.renderToTextureCommand = new CommandBuffer {name = "Render to texture"}; 191 this.renderToScreenCommand = new CommandBuffer {name = "Render to screen"}; 192 } 193 194 private void UpdateCommands(Brush b) 195 { 196 var brushMat = b.BrushMaterial; 197 var paintMat = b.PaintMaterial; 198 var r2t = this.renderToTextureCommand; 199 var r2s = this.renderToScreenCommand; 200 r2t.Clear(); 201 r2s.Clear(); 202 r2s.SetGlobalTexture(PaintTexId, this.paintRt); 203 foreach (var r in this.renderers) 204 { 205 var subMeshCount = 0; 206 switch (r) 207 { 208 case MeshRenderer mr: 209 subMeshCount = mr.GetComponent<MeshFilter>().sharedMesh.subMeshCount; 210 break; 211 case SkinnedMeshRenderer smr: 212 subMeshCount = smr.sharedMesh.subMeshCount; 213 break; 214 default: 215 subMeshCount = 1; 216 break; 217 } 218 219 for (var i = 0; i < subMeshCount; i++) 220 { 221 r2t.DrawRenderer(r, brushMat, i); 222 r2s.DrawRenderer(r, paintMat, i); 223 } 224 } 225 } 226 227 private void OnRenderObject() 228 { 229 // モデルの描画が終わった後のタイミングでペイントを上塗りする 230 if ((this.Brush == null) || (Camera.current == this.Brush.BrushCamera)) 231 { 232 return; 233 } 234 235 this.RenderToScreen(); 236 } 237 238 private void OnDestroy() 239 { 240 this.ReleaseTextures(); 241 } 242 243 private void ReleaseTextures() 244 { 245 this.paintRt.Release(); 246 } 247 } 248}
  • Brush

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

C#

1using System; 2using UniRx; 3using UnityEngine; 4 5namespace UnitychanPaint 6{ 7 [RequireComponent(typeof(Camera))] 8 public class Brush : MonoBehaviour 9 { 10 private static readonly int BrushColorId = Shader.PropertyToID("_BrushColor"); 11 private static readonly int BrushTexId = Shader.PropertyToID("_BrushTex"); 12 private static readonly int DepthTexId = Shader.PropertyToID("_DepthTex"); 13 private static readonly int BrushVpMatId = Shader.PropertyToID("_BrushVPMat"); 14 private static readonly int DrawBrushSpotId = Shader.PropertyToID("_DrawBrushSpot"); 15 16 [SerializeField] private Paintee target; 17 [SerializeField] private Material brushMaterial; // 2D展開図上にペイントするためのマテリアル 18 [SerializeField] private Material paintMaterial; // 3D画面上でモデル表面に塗りを上乗せするためのマテリアル 19 [SerializeField] private Color brushColor; 20 [SerializeField] private Texture brushShape; 21 22 private Transform brushTransform; 23 private Transform parentTransform; 24 private RenderTexture colorRt; 25 private RenderTexture depthRt; 26 private bool needsDrawUnpaintedBrushSpot; 27 28 public Camera BrushCamera { get; private set; } 29 30 // ブラシ色 31 public Color BrushColor 32 { 33 get => this.brushColor; 34 set 35 { 36 this.brushColor = value; 37 this.brushMaterial.SetColor(BrushColorId, value); 38 this.paintMaterial.SetColor(BrushColorId, value); 39 } 40 } 41 42 // ブラシテクスチャ(現状アルファしか使用していない) 43 public Texture BrushShape 44 { 45 get => this.brushShape; 46 set 47 { 48 this.brushShape = value; 49 this.brushMaterial.SetTexture(BrushTexId, value); 50 this.paintMaterial.SetTexture(BrushTexId, value); 51 } 52 } 53 54 // ブラシの位置にブラシ形状を投影描画する場合はtrueにする 55 // falseだと既にペイント済みのペイントテクスチャしか描画しない 56 public bool NeedsDrawUnpaintedBrushSpot 57 { 58 get => this.needsDrawUnpaintedBrushSpot; 59 set 60 { 61 this.needsDrawUnpaintedBrushSpot = value; 62 this.paintMaterial.SetFloat(DrawBrushSpotId, value ? 1.0f : 0.0f); 63 } 64 } 65 66 // 塗るターゲットとなるPaintee 67 public Paintee Target 68 { 69 get => this.target; 70 set 71 { 72 if (this.target != null) 73 { 74 this.target.Brush = null; 75 } 76 77 if (value != null) 78 { 79 value.Brush = this; 80 } 81 82 this.target = value; 83 } 84 } 85 86 public Material BrushMaterial 87 { 88 get 89 { 90 if (this.brushMaterial == null) 91 { 92 throw new InvalidOperationException($"{nameof(this.BrushMaterial)} is not set!"); 93 } 94 95 return this.brushMaterial; 96 } 97 } 98 99 public Material PaintMaterial 100 { 101 get 102 { 103 if (this.paintMaterial == null) 104 { 105 throw new InvalidOperationException($"{nameof(this.PaintMaterial)} is not set!"); 106 } 107 108 return this.paintMaterial; 109 } 110 } 111 112 public void UpdateViewProjectionMatrix(Material material) 113 { 114 var v = this.BrushCamera.worldToCameraMatrix; 115 var p = GL.GetGPUProjectionMatrix(this.BrushCamera.projectionMatrix, true); 116 material.SetMatrix(BrushVpMatId, p * v); 117 } 118 119 public void UpdateDepth() 120 { 121 this.BrushCamera.Render(); 122 }

投稿2019/04/16 21:36

Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問