何がしたいか
3Dモデルのテクスチャに直接書き込むようなペイントアプリを作成しようとしています。書き込む方法として、投影テクスチャマッピング用のシェーダーをGraphics.Blitを使いテクスチャに適用します。カメラからレイをとばしヒットした場所に書き込むようにするのですが、投影テクスチャリング用に疑似カメラを用意し、そのカメラのプロパティをシェーダーに渡すことで、特定の位置に書き込む(テクスチャを投影する)ようにしています。(投影テクスチャマッピングではなく、単にヒットした位置のUVをシェーダーに渡しその位置を中心にしてブラシ用のテクスチャも張り付ける方法も考え、試してみましたが、モデルのUVに大きく依存するので辞めました。)
何ができなかったか
投影テクスチャリングは実現できた(シェーダーコードは下にあります)のですが、Graphics.Blitで結果をテクスチャに直接反映しようとするとうまくいきません。一応塗られますが、位置が全然違います。
試したこと
まず投影テクスチャリングが失敗の原因なのかわからなかったので、以下のように、投影と投影後に動的にテクスチャに書き込むことができるスクリプトをつくってみました。このスクリプトで投影できることは確認できたのですが、投影した場所にテクスチャが書き込まれません。
スクリプトの説明と再現方法
投影されるモデルにこのスクリプトと投影用のシェーダーでつくったマテリアルを付けます。マテリアルのプロパティでブラシ用(投影される)テクスチャを適当に設定します。さらに、空のGameObjectをつくり、モデルのインスペクタからprojectorに設定します。色を設定し、プレイモードにすると、そのprojectorのオブジェクトのz方向に投影が行われます。インスペクタのねじマークからBakeを押すとGraphics.Blitによりテクスチャに投影結果が塗られます。
このスクリプトでは単に、オブジェクトのテクスチャーをレンダーテクスチャに置き換え、モデルのTransformからモデル変換行列、projectorからビュー行列、疑似カメラ用のプロパティからプロジェクション変換行列を毎フレームつくりシェーダー渡し、インスペクタからBakeが押された時だけGraphics.Blitするようにしています。本来はマウスクリックでレイを飛ばし、そのときだけ投影かつGraphics.Blitしますがこれは実験用のスクリプトですので。
Bakeしてみると、変な場所、左上あたり(uvでいう(0, 1)あたり)を中心に塗られてしまいます。projectorの位置やスケールを動かしてもほとんど塗られる位置に変更はありませんが、回転を変更するとすこし変化があることを確認しました。また、fovを変えると塗られる大きさが変わるみたいです。(ここは正しい?)どうやら、普通のレンダリングとGraphics.Blit時のものが違うためなのが原因みたいですが、どうやれば直るのかわからないです...
以下はBakeしたときの画像です。
上にある通り単純なUVを使ったペイントでも同じようにGraphics.Blitでテクスチャに適用していましたが正常に動いていたので、レンダーテクスチャの一連の流れ(メインテクスチャおきかえ、バッファを利用しBlitで書き込みなど)は間違っていないような気がします。渡す行列をBlitのために特別に変える必要があるのか(シェーダー内で?)よくわからないです。
Graphics.Blitを使わずにテクスチャに変更を加える方法もあるにはあるみたいですが、いろいろ制約がありそうなのでできればこれを使いたいです。例えばGraphics.DrawMeshNowがありますが、いくつかmeshがある場合その数だけ呼ばなければならなかったりSkinnedMeshRendererの場合BakeMeshしないといけないみたいです。一応想定しているモデルはSkinnedMeshRendererコンポーネントをいくつかもつものになります。
環境など
Unity 2018.3.0f1(アップデート予定)
VRアプリで考えてますが、とりあえずwindowsでプラットフォーム設定はデフォルトです。
何日かググって調べましたがどうしてもわかりませんでした。どうかよろしくお願いいたします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
public class ProjectionTest : MonoBehaviour {
[SerializeField] GameObject projector;
[SerializeField] Color brushColor;
[SerializeField] float fov = 20, aspect = 1, zNear = 0.01f, zFar = 1;
Material mat;
int MVPMatPropertyId, brushColorId;
RenderTexture renderTexture;
private void Awake() {
mat = GetComponent<Renderer>()?.material;
MVPMatPropertyId = Shader.PropertyToID("MVPMatForProjection");
brushColorId = Shader.PropertyToID("_BrushColor");
InitCanvas();
}
void InitCanvas() {
var mainTex = mat.mainTexture;
// generate RenderTexture
renderTexture = new RenderTexture(mainTex.width, mainTex.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default);
// copy the main texture to the RenderTexture
Graphics.Blit(mainTex, renderTexture);
// replace the main texture on the material with the RenderTexture
mat.mainTexture = renderTexture;
}
void SetMatProperties() {
if (mat && projector) {
Matrix4x4 modelMat = transform.localToWorldMatrix;
Matrix4x4 projMat = GL.GetGPUProjectionMatrix(Matrix4x4.Perspective(fov, aspect, zNear, zFar), true);
// camera space matches OpenGL convention: camera's forward is the negative Z axis, so negate all of the 3rd row values of the matrix
// https://docs.unity3d.com/ja/current/ScriptReference/Camera-worldToCameraMatrix.html
Matrix4x4 viewMat = Matrix4x4.TRS(projector.transform.position, projector.transform.rotation, projector.transform.lossyScale).inverse;
viewMat.m20 *= -1f;
viewMat.m21 *= -1f;
viewMat.m22 *= -1f;
viewMat.m23 *= -1f;
Matrix4x4 mvpMat = projMat * viewMat * modelMat; // this is the same as UNITY_MATRIX_MVP in shader writing
mat.SetMatrix(MVPMatPropertyId, mvpMat);
mat.SetColor(brushColorId, brushColor);
}
}
// Update is called once per frame
void Update() {
SetMatProperties();
}
[ContextMenu("Bake")]
void Bake() {
Debug.Log("paint");
var renderTextureBuffer = RenderTexture.GetTemporary(renderTexture.width, renderTexture.height); // buffer
Debug.Log("blit");
Graphics.Blit(renderTexture, renderTextureBuffer, mat); // first, copy renderTexture, which the painterMat will be applied to, to renderTextureBuffer because the texture can't be changed directly (and apply the material after copied
Graphics.Blit(renderTextureBuffer, renderTexture); // then, copy the buffer to renderTexture
}
}
Shader "Custom/Painter/ProjectionPaint"
{
Properties
{
_MainTex("Main Texture", 2D) = "white" {}
_BrushColor("Brush Color", Color) = (1, 1, 1, 1)
_ProjTex("Projection Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
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 _ProjTex;
float4 _MainTex_ST;
float4 _BrushColor;
uniform float4x4 MVPMatForProjection;
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.projUV = ComputeGrabScreenPos(mul(MVPMatForProjection, v.vertex));
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 projColor = fixed4(0, 0, 0, 0);
if (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(_ProjTex, 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
}
}
}
-
気になる質問をクリップする
クリップした質問は、後からいつでもマイページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
クリップを取り消します
-
良い質問の評価を上げる
以下のような質問は評価を上げましょう
- 質問内容が明確
- 自分も答えを知りたい
- 質問者以外のユーザにも役立つ
評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。
質問の評価を上げたことを取り消します
-
評価を下げられる数の上限に達しました
評価を下げることができません
- 1日5回まで評価を下げられます
- 1日に1ユーザに対して2回まで評価を下げられます
質問の評価を下げる
teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。
- プログラミングに関係のない質問
- やってほしいことだけを記載した丸投げの質問
- 問題・課題が含まれていない質問
- 意図的に内容が抹消された質問
- 過去に投稿した質問と同じ内容の質問
- 広告と受け取られるような投稿
評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。
質問の評価を下げたことを取り消します
この機能は開放されていません
評価を下げる条件を満たしてません
質問の評価を下げる機能の利用条件
この機能を利用するためには、以下の事項を行う必要があります。
- 質問回答など一定の行動
-
メールアドレスの認証
メールアドレスの認証
-
質問評価に関するヘルプページの閲覧
質問評価に関するヘルプページの閲覧
checkベストアンサー
+1
DrawMeshNowは避けたい...とのことですが、おそらくBlitのみではむしろ困難(あるいは無理?)のような気がします。
3D画面上でブラシによって塗られた領域と、そこに対応するテクスチャ上の位置を結び付けるにはモデルのUV情報が欲しいところですが、Blitでの描画は単なる四角い板を描いているようなものでしょうから、UV座標を使った対応付けができないんじゃないでしょうか?
すでにDrawMesh系統を使った例を色々調査なさったご様子ですが、基本的な方針はそれらをご参考にするのが妥当のように思います。そして、いくつもメッシュがあれば多分それぞれについて描画を行わせることになるだろうと思います。ただ、いくらかは効率よくすることは可能かもしれません。思いついただけで実験してはいませんが、例えばブラシの視錐台と交差するモデルだけを抜き出して処理する...とかでしょうかね?
SkinnedMeshRenderer対応についてですが、DrawMeshNowの代わりにCommandBufferのDrawRendererを使えばメッシュのベイクを行わなくても塗れそうな感じでした。
ご質問者さんのコードを下記のように改造し、どうなるか試してみました。
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEngine.Rendering;
public class ProjectionTest : MonoBehaviour
{
[SerializeField] GameObject projector;
[SerializeField] Color brushColor;
[SerializeField] Texture2D projectionTexture;
[SerializeField] float fov = 20, aspect = 1, zNear = 0.01f, zFar = 1;
Renderer[] renderers;
Material[][] materials;
RenderTexture[][] renderTextures;
CommandBuffer bakeCommand;
int VPMatPropertyId, brushColorId;
private void Awake()
{
var shader = Shader.Find("Custom/Painter/ProjectionPaint");
// 自身だけでなく子階層からもレンダラーを探す
renderers = GetComponentsInChildren<Renderer>();
// 一つのレンダラーが複数個のマテリアルを持っている場合も考慮した
var projTexId = Shader.PropertyToID("_ProjTex");
materials = renderers.Select(
r =>
{
return r.materials.Select(m =>
{
m.shader = shader;
m.SetTexture(projTexId, projectionTexture);
return m;
}).ToArray();
}).ToArray();
VPMatPropertyId = Shader.PropertyToID("VPMatForProjection");
brushColorId = Shader.PropertyToID("_BrushColor");
InitCanvas();
}
void InitCanvas()
{
// メインテクスチャと同数のレンダーテクスチャを用意する
var existingRenderTextures = new Dictionary<Texture, RenderTexture>();
renderTextures = materials.Select(
mArray =>
{
return mArray.Select(m => {
var mainTex = m.mainTexture;
RenderTexture rt;
if (!existingRenderTextures.TryGetValue(mainTex, out rt))
{
// generate RenderTexture
rt = new RenderTexture(
mainTex.width,
mainTex.height,
0,
RenderTextureFormat.ARGB32,
RenderTextureReadWrite.Default);
// copy the main texture to the RenderTexture
Graphics.Blit(mainTex, rt);
existingRenderTextures.Add(mainTex, rt);
}
// replace the main texture on the material with the RenderTexture
m.mainTexture = rt;
return rt;
}).ToArray();
}).ToArray();
// Bake内で行っていた作業に相当する操作を行うコマンドバッファを作成する
var c = new CommandBuffer();
var tempBufferId = Shader.PropertyToID("_TempBuffer");
for (var i = 0; i < renderTextures.Length; i++)
{
var rtArray = renderTextures[i];
var r = renderers[i];
var mArray = materials[i];
for (var j = 0; j < rtArray.Length; j++)
{
var rt = rtArray[j];
var m = mArray[j];
// 作業用のレンダーテクスチャ(_TempBuffer)を作成
c.GetTemporaryRT(tempBufferId, rt.descriptor);
// まず背景として、rtを加工せずにそのまま_TempBufferに描画
c.Blit(rt, tempBufferId);
// キーワードをオンにしてベイクモードに切り替え、j番目のサブメッシュを描画
c.EnableShaderKeyword("BAKE_PAINT");
c.SetRenderTarget(tempBufferId);
c.DrawRenderer(r, m, j);
c.DisableShaderKeyword("BAKE_PAINT");
// 結果をrtに戻す
c.Blit(tempBufferId, rt);
c.ReleaseTemporaryRT(tempBufferId);
}
}
bakeCommand = c;
}
void SetMatProperties()
{
if (projector)
{
Matrix4x4 projMat = GL.GetGPUProjectionMatrix(Matrix4x4.Perspective(fov, aspect, zNear, zFar), true);
// camera space matches OpenGL convention: camera's forward is the negative Z axis, so negate all of the 3rd row values of the matrix
// https://docs.unity3d.com/ja/current/ScriptReference/Camera-worldToCameraMatrix.html
Matrix4x4 viewMat = Matrix4x4.TRS(projector.transform.position, projector.transform.rotation, projector.transform.lossyScale).inverse;
viewMat.m20 *= -1f;
viewMat.m21 *= -1f;
viewMat.m22 *= -1f;
viewMat.m23 *= -1f;
// SkinnedMeshRendererはTransformのモデル行列とは異なった
// 加工済みの行列を使うはずなので、ここではモデル行列は合成しない
Matrix4x4 vpMat = projMat * viewMat;
foreach (var m in materials.SelectMany(mArray=>mArray))
{
m.SetMatrix(VPMatPropertyId, vpMat);
m.SetColor(brushColorId, brushColor);
}
}
}
// Update is called once per frame
void Update()
{
SetMatProperties();
}
[ContextMenu("Bake")]
void Bake()
{
Debug.Log("bake");
Graphics.ExecuteCommandBuffer(bakeCommand);
}
}
Shader "Custom/Painter/ProjectionPaint"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" { }
_BrushColor ("Brush Color", Color) = (1, 1, 1, 1)
_ProjTex ("Projection Texture", 2D) = "white" { }
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
// 裏を向いた面に対応するためカリングを切る
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// キーワードBAKE_PAINTあり・なしでマルチコンパイル
#pragma multi_compile _ BAKE_PAINT
#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 _ProjTex;
float4 _MainTex_ST;
float4 _BrushColor;
uniform float4x4 VPMatForProjection;
v2f vert(appdata v)
{
v2f o;
// ベイク時はメッシュのUV座標を頂点の位置と見なして描画させる
#ifdef BAKE_PAINT
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);
#else
o.vertex = UnityObjectToClipPos(v.vertex);
#endif
// SkinnedMeshRendererに対応するため、モデル行列はUnity組み込みのものを用いる
float4x4 mvpMat = mul(VPMatForProjection, 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);
if(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(_ProjTex, 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
}
}
}
まずMeshFilterの場合の実験として、下図のようなUVマップを持つスザンヌに対し...
ブラシの狙いを頭部右上周辺に合わせ...
ベイクすると下図の位置が赤く着色されました。
そしてSkinnedMeshRendererでもやってみようと思い、ユニティちゃんの右肩周辺を狙ってみましたが...
一応塗ることはできたものの、左半身にも色が飛び散ってしまいました。
ユニティちゃんにはUVを共有する面がかなりあるようで、このままでは半身だけを塗るのは無理そうです。
正しく塗るには、面が重ならないよう再配置したUV座標を用意してやる必要があるでしょう。もしかしたらライトマップ用UVをうまく使えば可能かもしれません。Unwrappingなんて機能もあるみたいですが、これはエディタ上でのみ使用可能なようですね。実行時にやりたい場合には、自前でUVを再配置してやることになりそうです...
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
15分調べてもわからないことは、teratailで質問しよう!
- ただいまの回答率 88.23%
- 質問をまとめることで、思考を整理して素早く解決
- テンプレート機能で、簡単に質問をまとめられる
2019/03/09 15:06
質問投稿してからもずっと模索しつづけていたのですが、自分でもBlitでは無理そうだなということがわかりました。Blitの内部がどうなっているのか調べていたら、おそらく以下のようなことをやっているみたいでした。
RenderTexture.active = destination;
material.SetTexture("_MainTex", source);
GL.PushMatrix();
GL.LoadOrtho();
material.SetPass(0);
GL.Begin(GL.QUADS);
GL.MultiTexCoord2(0, 0.0f, 0.0f);
GL.Vertex3(0.0f, 0.0f, 0.0f);
GL.MultiTexCoord2(0, 0.0f, 1.0f);
GL.Vertex3(0.0f, 1.0f, 0.0f);
GL.MultiTexCoord2(0, 1.0f, 1.0f);
GL.Vertex3(1.0f, 1.0f, 0.0f);
GL.MultiTexCoord2(0, 1.0f, 0.0f);
GL.Vertex3(1.0f, 0.0f, 0.0f);
GL.End();
GL.PopMatrix();
GLクラスのことはよくわからないのですが、おそらくRenderTextureの頂点位置をGL.Vertex3(0.0f, 0.0f, 0.0f);で設定していて、それがシェーダに渡されている=実際のモデルの頂点位置とは異なる。このために頂点位置を使って計算している投影シェーダがうまくいかないのかな、と思いました。(一応、他にも頂点のワールド位置を直接そのピクセルの色に設定するシェーダを書いて毎フレームBlitしてみましたが常に色固定だったのでそういうことなんだと思います。)
こういうわけで、DrawMesh使うしかないのかなと考えていたのですが、CommandBufferにDrawRendererなんてメソッドがあったんですね!!
いくつかメッシュがある場合の効率化についてですが、SkinnedMeshRendererを複数もち、かつIKなどで動かすかもしれないモデルをもともと想定していたので、それに対して効率よくレイキャストを行うために高速でBVHを構築するスクリプトをつくっていました。塗りたい範囲はそんなに広くないはずなので、塗る範囲をコライダーにし、そのBVHをつかって衝突したBoundがもつSkinnedMeshRendererを特定できるので「塗るSkinnedMeshRendererのみに対し処理をする」という課題はクリアできそうです。また、UVの重なりも、回答者様がおっしゃるようにUV2を使うことを想定していました。
すばらしい解答、本当にありがとうございます。まだ試してないのですが、さっそく使ってみたいと思います。
2019/03/09 16:19
position = v.uv * 2.0 - 1.0の部分もよくわからないのですが、さらにその(position, 0, 1)を頂点にしている理由がわからないです。
o.vertexがモデルの最終的な頂点の位置になるという認識なのですが、これではすべてのzが0になっておかしくなると思うのにならないのはなぜなんでしょう。。。
2019/03/10 03:01
ご存じでしたらすみませんが、最終的に各頂点座標は出力先(画面やレンダーテクスチャ)の中心が(0, 0, 0)、左端のxが-1、右端のxが+1、下端のyが-1、上端のyが+1になる座標系に押し込めてやることになります。zについてはOpenGLやDirectXで微妙に違うそうですが、ともかく-1~+1あるいは0~+1の範囲に詰められます。
普段の3D描画だと、基本的にはメッシュの元々の頂点座標にモデル・ビュー・プロジェクション変換を施して、この正規化された空間に押し縮めていることになります。このへんの過程については色々な解説サイトがあるかと思いますが、例えば http://www.opengl-tutorial.org/jp/beginners-tutorials/tutorial-3-matrices/ の後半には図入りの解説があってイメージしやすそうでした。
今回の場合、通常の描画時とベイク描画時でフラグメントシェーダーは同じ...つまりメッシュへの色付けの仕方は同じまま、頂点の位置を通常時はいつもの立体的に見える位置に、ベイク時はUV展開図の位置になるようにしています。頂点の位置だけ展開図の形にすることで、塗られる位置もその場所に対応するUV展開図上の位置になる...という理屈です。
すでにご覧になったかもしれませんが、http://edom18.hateblo.jp/entry/2018/11/08/084143 の記事の中程にある https://twitter.com/i/status/1059671503115939840 のムービーがわかりやすく感じました。
先ほど申し上げたモデル・ビュー・プロジェクション変換ですが、これはべつに必ず行わなければならないものではなく、とにかく最終的に-1~+1の直方体に押し込めた座標を出力しさえすればいいのです。
メッシュに埋め込まれたUV座標は、通常は縦横それぞれ0~1の2次元座標になっていると思います(テクスチャが端でクリッピングされたり繰り返されたりすることを見越して、あえてはみ出した座標を使うテクニックもありますが...そのようなモデルも、今回のやり方では正しく塗ることができないでしょう)。ですので、それを単純に縦横2倍に引き延ばしてから中心が(0, 0)になるようにずらしてやれば0~+1の範囲が-1~+1の範囲に変換され、描画先...つまりレンダーテクスチャ全面にUV展開図を描くことができるというわけです。
2019/03/14 16:47