やりたいこと
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で画面に表示してみたのですが、うまく深度テクスチャがコピーできなかったです...
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class DepthProjection : MonoBehaviour
{
[SerializeField] Color brushColor;
[SerializeField] Texture brushTex;
[SerializeField] GameObject projector;
[SerializeField] float zNear = 0.01f, zFar = 1, orthoSize = 0.5f;
[SerializeField] Material paintMat;
Material objMat;
Renderer renderer;
RenderTexture modelMainRT, colorBufferRT, depthBufferRT;
public RenderTexture depthTex
{
get
{
return depthBufferRT;
}
}
int ProjectorVPMat, brushColorId, projTexId, depthTexId;
CommandBuffer bakeCommand;
Camera projCam;
private void OnValidate()
{
SetProjCam();
}
void SetProjCam()
{
if (projCam)
{
projCam.orthographic = true;
projCam.orthographicSize = orthoSize;
projCam.nearClipPlane = zNear;
projCam.farClipPlane = zFar;
projCam.aspect = 1;
}
}
private void Awake()
{
renderer = GetComponent<Renderer>();
objMat = renderer?.material;
ProjectorVPMat = Shader.PropertyToID("ProjectorVPMat");
brushColorId = Shader.PropertyToID("_BrushColor");
projTexId = Shader.PropertyToID("_BrushTex");
depthTexId = Shader.PropertyToID("_DepthTex");
projCam = projector?.GetComponent<Camera>();
if (projCam == null)
{
Debug.LogAssertion("projector needs Camera Component");
}
SetProjCam();
InitProjectorAndDepthTexture();
InitCommand();
}
void InitProjectorAndDepthTexture()
{
if (objMat)
{
var mainTex = objMat.mainTexture;
if (mainTex && projCam)
{
modelMainRT = new RenderTexture(mainTex.width, mainTex.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default);
projCam.depthTextureMode = DepthTextureMode.Depth;
colorBufferRT = new RenderTexture(projCam.pixelWidth, projCam.pixelHeight, 0, RenderTextureFormat.ARGB32);
colorBufferRT.Create();
depthBufferRT = new RenderTexture(projCam.pixelWidth, projCam.pixelHeight, 24, RenderTextureFormat.Depth);
depthBufferRT.Create();
projCam.SetTargetBuffers(colorBufferRT.colorBuffer, depthBufferRT.depthBuffer);
Graphics.Blit(mainTex, modelMainRT);
objMat.mainTexture = modelMainRT;
if (paintMat)
{
paintMat.mainTexture = modelMainRT;
paintMat.SetTexture(depthTexId, depthBufferRT);
}
}
}
}
void InitCommand()
{
if (renderer & modelMainRT & paintMat)
{
var c = new CommandBuffer();
c.name = "Bake Projection Command";
var RTBufferId = Shader.PropertyToID("_RTBuffer");
c.GetTemporaryRT(RTBufferId, modelMainRT.descriptor);
c.Blit(modelMainRT, RTBufferId);
c.SetRenderTarget(RTBufferId);
c.DrawRenderer(renderer, paintMat);
c.Blit(RTBufferId, modelMainRT);
c.ReleaseTemporaryRT(RTBufferId);
bakeCommand = c;
}
}
void SetMatProperties()
{
if (projector && paintMat && projCam != null)
{
Matrix4x4 projMat = GL.GetGPUProjectionMatrix(projCam.projectionMatrix, true);
Matrix4x4 viewMat = projCam.worldToCameraMatrix;
Matrix4x4 vpMat = projMat * viewMat;
paintMat.SetMatrix(ProjectorVPMat, vpMat);
paintMat.SetColor(brushColorId, brushColor);
paintMat.SetTexture(projTexId, brushTex);
}
}
// Update is called once per frame
void Update()
{
SetMatProperties();
Bake();
}
public void Bake()
{
if (bakeCommand != null)
{
Graphics.ExecuteCommandBuffer(bakeCommand);
}
}
private void OnDestroy()
{
ReleaseAllRT();
}
void ReleaseAllRT()
{
ReleaseRT(modelMainRT);
ReleaseRT(colorBufferRT);
ReleaseRT(depthBufferRT);
}
void ReleaseRT(RenderTexture rt)
{
if (rt)
{
rt.Release();
}
}
}
深度を利用した投影テクスチャマッピングのシェーダーコードです。
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だと思います。)
プラットフォームはデフォルトのものです。
よろしくお願いいたします。
-
気になる質問をクリップする
クリップした質問は、後からいつでもマイページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
クリップを取り消します
-
良い質問の評価を上げる
以下のような質問は評価を上げましょう
- 質問内容が明確
- 自分も答えを知りたい
- 質問者以外のユーザにも役立つ
評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。
質問の評価を上げたことを取り消します
-
評価を下げられる数の上限に達しました
評価を下げることができません
- 1日5回まで評価を下げられます
- 1日に1ユーザに対して2回まで評価を下げられます
質問の評価を下げる
teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。
- プログラミングに関係のない質問
- やってほしいことだけを記載した丸投げの質問
- 問題・課題が含まれていない質問
- 意図的に内容が抹消された質問
- 過去に投稿した質問と同じ内容の質問
- 広告と受け取られるような投稿
評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。
質問の評価を下げたことを取り消します
この機能は開放されていません
評価を下げる条件を満たしてません
質問の評価を下げる機能の利用条件
この機能を利用するためには、以下の事項を行う必要があります。
- 質問回答など一定の行動
-
メールアドレスの認証
メールアドレスの認証
-
質問評価に関するヘルプページの閲覧
質問評価に関するヘルプページの閲覧
checkベストアンサー
+1
深度バッファを使うというのは的確なアイディアだと思います。確かに状況がシャドウマッピングとよく似ていますね。
おそらく、うまくいかなかった原因はデプステクスチャのサンプリング位置の狂いのような気がします。それを含め、いくつか変更を加えてみました。まずC#スクリプト側は...
- デプステクスチャの解像度として
projCam.pixelWidth
とprojCam.pixelHeight
を使うと、サイズは画面解像度と同じになるかと思います。
それはそれでいいとも思いますが、個人的には画面サイズによってエッジの精密さが変わってしまうのが気になり、さしあたりbrushTex.width
とbrushTex.height
を使ってブラシテクスチャと同サイズにしました。これでは荒すぎるようでしたら、brushTex.width * 4
といった風に何倍かしてやるのもいいでしょう。 projCam
のenabled
をSetProjCam
内でfalse
に変更して自動的レンダリングはさせないようにし、代わりにベイク用コマンドバッファ実行部分の手前で明示的にレンダリングさせるようにしました。順序的にはこちらの方が自然じゃないでしょうかね?
当初のコードですと、レンダリングされたデプスがベイク結果に反映されるのは次回ベイク時...つまり次のフレームになると思われます。projCam
の描画に綺麗なシェーディングを施す必要はないので、projCam.renderingPath
をRenderingPath.VertexLit
に差し替えました。スカイボックスもいらないでしょうから切っています。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class DepthProjection : MonoBehaviour
{
[SerializeField] Color brushColor;
[SerializeField] Texture brushTex;
[SerializeField] GameObject projector;
[SerializeField] float zNear = 0.01f, zFar = 1, orthoSize = 0.5f;
[SerializeField] Material paintMat;
Material objMat;
Renderer renderer;
RenderTexture modelMainRT, colorBufferRT, depthBufferRT;
public RenderTexture depthTex
{
get
{
return depthBufferRT;
}
}
int ProjectorVPMat, brushColorId, projTexId, depthTexId;
CommandBuffer bakeCommand;
Camera projCam;
private void OnValidate()
{
SetProjCam();
}
void SetProjCam()
{
if (projCam)
{
projCam.orthographic = true;
projCam.orthographicSize = orthoSize;
projCam.nearClipPlane = zNear;
projCam.farClipPlane = zFar;
projCam.aspect = 1;
// カメラコンポーネントを非アクティブ化して自動レンダリングをさせなくする
// また、projCam上でレンダリングする際のパスをVertexLitに差し替える
// スカイボックスも描画不要
projCam.enabled = false;
projCam.renderingPath = RenderingPath.VertexLit;
projCam.clearFlags = CameraClearFlags.Depth;
}
}
private void Awake()
{
renderer = GetComponent<Renderer>();
objMat = renderer?.material;
ProjectorVPMat = Shader.PropertyToID("ProjectorVPMat");
brushColorId = Shader.PropertyToID("_BrushColor");
projTexId = Shader.PropertyToID("_BrushTex");
depthTexId = Shader.PropertyToID("_DepthTex");
projCam = projector?.GetComponent<Camera>();
if (projCam == null)
{
Debug.LogAssertion("projector needs Camera Component");
}
SetProjCam();
InitProjectorAndDepthTexture();
InitCommand();
}
void InitProjectorAndDepthTexture()
{
if (objMat)
{
var mainTex = objMat.mainTexture;
if (mainTex && projCam)
{
modelMainRT = new RenderTexture(mainTex.width, mainTex.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default);
projCam.depthTextureMode = DepthTextureMode.Depth;
// デプステクスチャのサイズをブラシテクスチャと同サイズに変更
var depthBufferWidth = brushTex.width;
var depthBufferHeight = brushTex.height;
colorBufferRT = new RenderTexture(depthBufferWidth, depthBufferHeight, 0, RenderTextureFormat.ARGB32);
colorBufferRT.Create();
depthBufferRT = new RenderTexture(depthBufferWidth, depthBufferHeight, 24, RenderTextureFormat.Depth);
depthBufferRT.Create();
projCam.SetTargetBuffers(colorBufferRT.colorBuffer, depthBufferRT.depthBuffer);
Graphics.Blit(mainTex, modelMainRT);
objMat.mainTexture = modelMainRT;
if (paintMat)
{
paintMat.mainTexture = modelMainRT;
paintMat.SetTexture(depthTexId, depthBufferRT);
}
}
}
}
void InitCommand()
{
if (renderer & modelMainRT & paintMat)
{
var c = new CommandBuffer();
c.name = "Bake Projection Command";
var RTBufferId = Shader.PropertyToID("_RTBuffer");
c.GetTemporaryRT(RTBufferId, modelMainRT.descriptor);
c.Blit(modelMainRT, RTBufferId);
c.SetRenderTarget(RTBufferId);
c.DrawRenderer(renderer, paintMat);
c.Blit(RTBufferId, modelMainRT);
c.ReleaseTemporaryRT(RTBufferId);
bakeCommand = c;
}
}
void SetMatProperties()
{
if (projector && paintMat && projCam != null)
{
Matrix4x4 projMat = GL.GetGPUProjectionMatrix(projCam.projectionMatrix, true);
Matrix4x4 viewMat = projCam.worldToCameraMatrix;
Matrix4x4 vpMat = projMat * viewMat;
paintMat.SetMatrix(ProjectorVPMat, vpMat);
paintMat.SetColor(brushColorId, brushColor);
paintMat.SetTexture(projTexId, brushTex);
}
}
// Update is called once per frame
void Update()
{
SetMatProperties();
Bake();
}
public void Bake()
{
// このタイミングでprojCamを手動描画
projCam.Render();
if (bakeCommand != null)
{
Graphics.ExecuteCommandBuffer(bakeCommand);
}
}
private void OnDestroy()
{
ReleaseAllRT();
}
void ReleaseAllRT()
{
ReleaseRT(modelMainRT);
ReleaseRT(colorBufferRT);
ReleaseRT(depthBufferRT);
}
void ReleaseRT(RenderTexture rt)
{
if (rt)
{
rt.Release();
}
}
}
次に、シェーダー側は...
depthVertex
は廃止しました。ブラシテクスチャとデプステクスチャはともに同じUV座標をサンプリングするべきだと思いますので、ともにprojUV
を使用することにしました。- フラグメントの前後判定の際に、環境によるクリップ空間の違いを考慮するようにしてみました。おそらくこれでいけるかとは思うのですが、もし前後判定の狂いがあるようでしたら、どのような描画結果になってしまったか、グラフィックスシステムは何を使っているかコメントいただければ、可能であれば直してみようと思います。
ご提示いただいた渋谷ほととぎす通信さんの製作例の途中に、デプステクスチャの内容を画面上に表示してみせている画像がありますが、そこではカメラに近い部分ほど色が暗くなっています。一方、【Unity】【シェーダ】カメラから見た深度を描画する - LIGHT11にもデプスの可視化を試みている例がありますが、こちらは逆に近い部分が明るくなっています。プラットフォーム特有のレンダリングの違い - Unity マニュアルの「クリップスペース座標」や「シェーダーの深度 (Z) の向き」をご覧いただくと、ごちゃごちゃぶりを察していただけるでしょうか...
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;
};
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);
// 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
{
// 環境に応じて奥行き値を加工する際に使う
#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)
{
// _BrushTexも_DepthTexも同じ位置をサンプリングする
projColor = tex2D(_BrushTex, i.projUV);
depth = tex2D(_DepthTex, i.projUV).r;
}
}
// まずブラシ視点からのクリップ座標z(w除算済み)とdepthを、奥が1・手前が0となるよう加工する
float near = UNITY_NEAR_CLIP_VALUE;
float far = 1.0 - inverseZ;
float normalizedZ = (i.projUV.z - near) / (far - near);
float normalizedDepth = inverseZ + (1.0 - 2.0 * inverseZ) * depth;
// 両者を比較して前後を判断する
// 判定に落ちたフラグメントはclipで破棄する
// また、Zファイティング現象防止のため、判定値は少しだけ増やしてやる
clip(normalizedDepth - normalizedZ + 0.001);
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
}
}
}
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
15分調べてもわからないことは、teratailで質問しよう!
- ただいまの回答率 88.32%
- 質問をまとめることで、思考を整理して素早く解決
- テンプレート機能で、簡単に質問をまとめられる
質問への追記・修正、ベストアンサー選択の依頼
stdio
2019/03/25 10:07
同じUV情報を使ってないならテクスチャ新調した方が速な感じがするのですが...
torano
2019/03/25 10:15
どういうことでしょうか?
stdio
2019/03/25 10:27
冷たいアドバイスになりますが、
この程度の事が分からずに深度バッファをしているなら、もう一度勉強してきた方が良いですよ。