やりたいこと
3Dモデルのテクスチャに直接書き込むようなペイントアプリを作成したいと思っています。方法としては、メインカメラの他にカメラを用意しそこからレイをとばしヒットした場所に投影テクスチャマッピングしてレンダーテクスチャに直接書き込もうと考えています。(毎フレームテクスチャでスタンプしていくようなイメージです。)
投影テクスチャマッピングしたものをそのモデルのテクスチャに反映する部分で苦戦していたのですが、ありがたいことにその問題はここで質問し、解決することができました。
[Unity]Graphics.Blitで投影テクスチャマッピングシェーダーをテクスチャに反映できない
問題点
しかし次の問題として、投影テクスチャマッピングの仕様上カメラから見えない場所(モデルの凹凸に隠れる場所や、裏側など)にも投影されてしまうことに気づきました。以下のようなものです。
Gameビューをマウスでクリックし、そこからレイをとばしてヒットポイントに塗っているのですが、ご覧の通り塗られるべきでない場所にも塗られてしまっているので、解決したいです。
試したこと
いろいろと調べてみた結果、深度バッファを使えば解決できそうだということがわかりました。カメラからピクセルまでの距離と深度情報を比べて深度のほうが小さければ塗らない、といった具合です。そして幸運にも似たようなことをやっている人を発見しました。
Unity デプスシャドウ技法を自前で書いて影を落としてみる
しかし、これを真似て実装してみたのですがうまくいきませんでした。
スクリプトと再現方法
前回の質問したときのスクリプトを、解決法にそって改善し、自分なりに上のことを実現しようとしたスクリプトです。前回と同様、塗られるモデルにこのスクリプトをアタッチし、各種パラメータを設定します。projectorには空のゲームオブジェクトにCameraコンポーネントを付けたものを入れます。Cameraの設定はOcclusion Culling, Allow HDR, Allow MSAAをオフにしてます。(orthoSizeなどはスクリプトから調節するようにしてます。)
前回と違い、今回は深度情報を使いたかったので疑似ではなくCameraコンポーネントを使用しています。またモデルには他の適当なマテリアル(スタンダードシェーダなど)を使用しています。Bakeファンクションで実際の塗りが発生しますが、これはテスト用のスクリプトなので毎フレーム塗っています。
先のデプスシャドウの実装では、予めRenderTextureをつくりCameraのTargetTextureにつけているようですが、この方法は自分ではなぜかうまく深度値がとれなかったのでスクリプトで動的にバッファを作成しカメラをDepthモードに切り替えています。
おそらくカメラからピクセルまでの距離か深度情報どちらかの値が間違っていると思うのですが、もしかしたら深度がうまくとれていないのかもしれません...
深度を扱うのは今回がはじめてで試行錯誤しながらやっていて、よくある例のようにポストエフェクトでメインカメラからの深度を直接画面に反映する方法で深度テクスチャの方法はうまくいっていました。しかし今回のようにシーンにカメラを二つ使用し、サブカメラのほうで深度をとろうとするとうまくいかなかったです。そもそも深度テクスチャはほぼ真っ黒なので、Inspectorだけではうまく深度とれてるか確認できず、デバッグのために深度テクスチャをGraphics.Blitで他のシェーダ(rの値が0と1意外の値のとき色をはっきりさせるもの)を使ってコピーし、uGUIで画面に表示してみたのですが、うまく深度テクスチャがコピーできなかったです...
C#
1using System.Collections; 2using System.Collections.Generic; 3using UnityEngine; 4using UnityEngine.Rendering; 5 6public class DepthProjection : MonoBehaviour 7{ 8 [SerializeField] Color brushColor; 9 [SerializeField] Texture brushTex; 10 [SerializeField] GameObject projector; 11 [SerializeField] float zNear = 0.01f, zFar = 1, orthoSize = 0.5f; 12 [SerializeField] Material paintMat; 13 14 Material objMat; 15 Renderer renderer; 16 RenderTexture modelMainRT, colorBufferRT, depthBufferRT; 17 public RenderTexture depthTex 18 { 19 get 20 { 21 return depthBufferRT; 22 } 23 } 24 25 int ProjectorVPMat, brushColorId, projTexId, depthTexId; 26 CommandBuffer bakeCommand; 27 Camera projCam; 28 29 private void OnValidate() 30 { 31 SetProjCam(); 32 } 33 34 void SetProjCam() 35 { 36 if (projCam) 37 { 38 projCam.orthographic = true; 39 projCam.orthographicSize = orthoSize; 40 projCam.nearClipPlane = zNear; 41 projCam.farClipPlane = zFar; 42 projCam.aspect = 1; 43 } 44 } 45 46 private void Awake() 47 { 48 renderer = GetComponent<Renderer>(); 49 objMat = renderer?.material; 50 ProjectorVPMat = Shader.PropertyToID("ProjectorVPMat"); 51 brushColorId = Shader.PropertyToID("_BrushColor"); 52 projTexId = Shader.PropertyToID("_BrushTex"); 53 depthTexId = Shader.PropertyToID("_DepthTex"); 54 55 projCam = projector?.GetComponent<Camera>(); 56 57 if (projCam == null) 58 { 59 Debug.LogAssertion("projector needs Camera Component"); 60 } 61 62 SetProjCam(); 63 64 InitProjectorAndDepthTexture(); 65 InitCommand(); 66 67 } 68 69 void InitProjectorAndDepthTexture() 70 { 71 if (objMat) 72 { 73 var mainTex = objMat.mainTexture; 74 75 if (mainTex && projCam) 76 { 77 modelMainRT = new RenderTexture(mainTex.width, mainTex.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default); 78 79 projCam.depthTextureMode = DepthTextureMode.Depth; 80 81 colorBufferRT = new RenderTexture(projCam.pixelWidth, projCam.pixelHeight, 0, RenderTextureFormat.ARGB32); 82 colorBufferRT.Create(); 83 depthBufferRT = new RenderTexture(projCam.pixelWidth, projCam.pixelHeight, 24, RenderTextureFormat.Depth); 84 depthBufferRT.Create(); 85 86 projCam.SetTargetBuffers(colorBufferRT.colorBuffer, depthBufferRT.depthBuffer); 87 88 Graphics.Blit(mainTex, modelMainRT); 89 objMat.mainTexture = modelMainRT; 90 91 if (paintMat) 92 { 93 paintMat.mainTexture = modelMainRT; 94 paintMat.SetTexture(depthTexId, depthBufferRT); 95 } 96 } 97 } 98 } 99 100 void InitCommand() 101 { 102 if (renderer & modelMainRT & paintMat) 103 { 104 105 var c = new CommandBuffer(); 106 c.name = "Bake Projection Command"; 107 108 var RTBufferId = Shader.PropertyToID("_RTBuffer"); 109 110 c.GetTemporaryRT(RTBufferId, modelMainRT.descriptor); 111 c.Blit(modelMainRT, RTBufferId); 112 c.SetRenderTarget(RTBufferId); 113 c.DrawRenderer(renderer, paintMat); 114 c.Blit(RTBufferId, modelMainRT); 115 c.ReleaseTemporaryRT(RTBufferId); 116 117 bakeCommand = c; 118 } 119 } 120 121 void SetMatProperties() 122 { 123 if (projector && paintMat && projCam != null) 124 { 125 Matrix4x4 projMat = GL.GetGPUProjectionMatrix(projCam.projectionMatrix, true); 126 Matrix4x4 viewMat = projCam.worldToCameraMatrix; 127 128 Matrix4x4 vpMat = projMat * viewMat; 129 130 paintMat.SetMatrix(ProjectorVPMat, vpMat); 131 paintMat.SetColor(brushColorId, brushColor); 132 paintMat.SetTexture(projTexId, brushTex); 133 } 134 } 135 136 // Update is called once per frame 137 void Update() 138 { 139 SetMatProperties(); 140 Bake(); 141 } 142 143 public void Bake() 144 { 145 if (bakeCommand != null) 146 { 147 Graphics.ExecuteCommandBuffer(bakeCommand); 148 } 149 } 150 151 private void OnDestroy() 152 { 153 ReleaseAllRT(); 154 } 155 156 void ReleaseAllRT() 157 { 158 ReleaseRT(modelMainRT); 159 ReleaseRT(colorBufferRT); 160 ReleaseRT(depthBufferRT); 161 } 162 163 void ReleaseRT(RenderTexture rt) 164 { 165 if (rt) 166 { 167 rt.Release(); 168 } 169 } 170} 171
深度を利用した投影テクスチャマッピングのシェーダーコードです。
Shader "Painter/Test/DepthProjectionTest" { Properties { _MainTex("Main Texture", 2D) = "white" { } _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 { // 裏を向いた面に対応するためカリングを切る Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex: POSITION; float2 uv: TEXCOORD0; }; struct v2f { float2 uv: TEXCOORD0; float4 vertex: SV_POSITION; float4 projUV: TEXCOORD1; float4 depthVertex : TEXCOORD2; }; sampler2D _MainTex; sampler2D _BrushTex; sampler2D _DepthTex; float4 _MainTex_ST; float4 _BrushColor; uniform float4x4 ProjectorVPMat; v2f vert(appdata v) { v2f o; // ベイク時はメッシュのUV座標を頂点の位置と見なして描画させる float2 position = v.uv * 2.0 - 1.0; #if UNITY_UV_STARTS_AT_TOP // DirectX系の場合は上下を逆転させる position.y *= -1.0; #endif o.vertex = float4(position, 0.0, 1.0); o.depthVertex = ComputeGrabScreenPos(mul(ProjectorVPMat, v.vertex)); // SkinnedMeshRendererに対応するため、モデル行列はUnity組み込みのものを用いる float4x4 mvpMat = mul(ProjectorVPMat, unity_ObjectToWorld); o.projUV = ComputeGrabScreenPos(mul(mvpMat, v.vertex)); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 projColor = fixed4(0, 0, 0, 0); // depth textureから深度値をサンプリング。線形になるようLinear01Depthを使ったほうがいい? fixed4 depthColor = tex2D(_DepthTex, i.depthVertex.xy); float diff = i.depthVertex.z - depthColor.r; if (diff <= 0 && i.projUV.w > 0.0) { // projection to screen space i.projUV.x /= i.projUV.w; i.projUV.y /= i.projUV.w; if (i.projUV.x >= 0 && i.projUV.x <= 1 && i.projUV.y >= 0 && i.projUV.y <= 1) { projColor = tex2D(_BrushTex, i.projUV); } } fixed4 mainColor = tex2D(_MainTex, i.uv); // 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 } } }
他
思いついた方法が新しくカメラを用意し深度テクスチャを使う、というものだったのですが、もし他に良い方法があれば教えてほしいです。できれば二度レンダリングしたくないので...
環境
Unity 2018.3.9f1で作成したプロジェクトです。(レンダリング設定はforwardだと思います。)
プラットフォームはデフォルトのものです。
よろしくお願いいたします。
回答1件
あなたの回答
tips
プレビュー