【Unity】ペンキをぶっかけた人型を作ってみた
深度チェック付きプロジェクター作成
Unity 標準の Projector とは
Projector により、Material を錐台と交錯するすべてのオブジェクトに投影できます。このマテリアルは、投影効果を正しく機能させるための特殊なタイプのシェーダーです。提供されている Projector/Light と Projector/Multiply シェーダーの使用法の例については、Unity のスタンダードアセットのプロジェクタープレハブを参照してください。
https://docs.unity3d.com/ja/current/Manual/class-Projector.html
スポットライトがテクスチャーを参照しつつ描画。と思ってもらえればよいです。
このままでも実用的ですが、標準では遮蔽物も透視して錐台内 全てに描画されてしまいます。
今回やることは、視点から遮蔽物の裏には描画しないプロジェクターです。ペンキをぶっかけられて後ろの壁には人の跡が残る、あれです。
ネットを調べても見つからなかったので作ってみました。
構想
- プロジェクターにカメラを付けて深度バッファを生成
- 深度バッファと比較しながらプロジェクターをレンダリング
1,コンポーネントの用意
まず 標準のProjectorのついたオブジェクトにカメラコンポーネントを追加します。
2,深度バッファテクスチャーの用意
次に深度バッファレンダリング用テクスチャを用意してカメラのTarget Texture に設定します。スクリプトで動的に生成しても良いですが、今回はスクリプトをシンプルにするため先に生成しておきます。
Project > 右クリックからのコンテキストメニュー > Create > Render Texture
から生成。初期状態は RAWな RGBA テクスチャなので 深度バッファ用に編集。
3,プロジェクターカメラの設定
Clear Flags は Depth only に設定。
Projection は平行投影 の Orthographic を設定。
Target Texture に先程作成した深度バッファテクスチャを設定。
Rendering Path は深度描画時の Forward に設定。
Orthographicのサイズはメートルのようです。
ここまでで、深度バッファに描画するオブジェクトができました。
実行してみると、深度バッファに内容が書き込まれるのがわかります。
あとはこいつを参照しつつ、メインのテクスチャを描画するシェーダーを書いて、Projector に設定すれば完成です。
4,プロジェクターのシェーダー作成
深度バッファを見ながらカラーテクスチャを描画するシェーダーを書きます。
DepthProjector.shader
/*****************************************************************************
* $Id: $
* 深度チェック付きプロジェクションシェーダー
*****************************************************************************/
Shader "Custom/DepthProjector"
{
Properties
{
_MainTex( "Source", 2D) = "white" {}
_DepthTex( "DepthTexture", 2D) = "white" {}
}
SubShader
{
Cull Back
Fog { Mode Off }
Pass
{
Tags { "RenderType"="Opaque" "Queue"="Geometry+10"}
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
struct appdata
{
float4 vertex : POSITION;
float4 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 uv : TEXCOORD0;
float4 projector_vertex : TEXCOORD1; // u, v, depth
fixed4 diff : COLOR0; // 拡散ライティングカラー
};
float4x4 unity_Projector;
float4x4 _DepthVP;
float3 _DepthDir;
sampler2D _MainTex; // カラーテクスチャー
sampler2D _DepthTex; // 深度テクスチャー
// 頂点シェーダー
v2f vert( appdata v)
{
v2f o;
// メインカメラからの座標
o.vertex = UnityObjectToClipPos( v.vertex);
o.uv = mul( unity_Projector, v.vertex);
// プロジェクター視点の座標
float3 posv = mul( UNITY_MATRIX_M, v.vertex);
o.projector_vertex = mul( _DepthVP, float4( posv, 1));
// プロジェクター視点の法線
float3 normalv = mul( UNITY_MATRIX_M, v.normal);
o.uv.z = dot( normalv, _DepthDir);
//法線とライト方向間のドット積
half3 worldNormal = UnityObjectToWorldNormal(v.normal);
half nl = max( 0, dot( worldNormal, _WorldSpaceLightPos0.xyz));
// ライトカラーの積
o.diff = nl * _LightColor0;
return o;
}
// フラグメントシェーダー
fixed4 frag( v2f i) : SV_Target
{
// クリッピング
if( abs( i.projector_vertex.x / i.projector_vertex.w) > 1)
{
discard; // 中止
}
if( abs( i.projector_vertex.y / i.projector_vertex.w) > 1)
{
discard; // 中止
}
// 法線チェック
if( i.uv.z < 0.0)
{
discard; // 中止
}
float depth = i.projector_vertex.z;
float4 depth_tex = tex2Dproj( _DepthTex, i.uv);
// 差が大きいなら最前面ではないだろう
if( abs( depth - depth_tex.r) >= 0.01)
{
discard; // 中止
}
fixed4 col = tex2Dproj( _MainTex, i.uv);
col *= i.diff;
return col;
}
ENDCG
}
}
}
内容はプロジェクターの視点からの距離を計算し、深度バッファと見比べて合格だった場合にピクセルを描画するといった感じです。
o.uv = mul( unity_Projector, v.vertex);
ここで珍しい変数 unity_Projector がありますが、これはプロジェクター用の変換行列になります。unity_ProjectorClip という変数もありますが、使い方がイマイチわかりませんでした。
5,シェーダー用のマテリアルを生成
Assets > Create > Material で新規マテリアルを生成し、Shader を先程の Custom/DepthProjector にします。
その後 Source Texture にぶっかけたいテクスチャーを、DepthTexture に先程生成した 深度テクスチャを設定します。
6,シェーダーに情報を送り込むスクリプトの作成
DepthProjector.cs
/*****************************************************************************
* $Id: $
* 深度チェック付きプロジェクター
*****************************************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DepthProjector : MonoBehaviour {
// プロジェクター本体
private Projector projector{ get; set; }
// プロジェクターで描画するマテリアル
private Material projectorMaterial{ get; set; }
// プロジェクタに付いているカメラ
private Camera depthCamera{ get; set; }
//----------------------------------------------------------------------------
void Start()
{
projector = GetComponent<Projector>();
projectorMaterial = projector?.material;
depthCamera = GetComponent<Camera>();
}
//----------------------------------------------------------------------------
// 描画前に呼ばれる
void OnPreRender()
{
// プロジェクターから見た射影行列をシェーダーに送る。
var decalVMatrix = depthCamera.worldToCameraMatrix;
var decalPMatrix = GL.GetGPUProjectionMatrix( depthCamera.projectionMatrix, false);
var decalVP = decalPMatrix * decalVMatrix;
projectorMaterial?.SetMatrix( "_DepthVP", decalVP);
// ついでに視線ベクトルも送る。
var decalDir = depthCamera.transform.TransformDirection( new Vector3( 0, 0, 1));
projectorMaterial?.SetVector( "_DepthDir", decalDir);
}
}
あとは、このスクリプトをプロジェクターのオブジェクトに追加すると完成です。
内容は毎フレームプロジェクターに付いているカメラ行列を計算し、シェーダーに送り続けるだけのシンプルなものです。この辺りも省いて、シェーダー側で処理したかったのですが、方法が思いつきませんでした。妥協策です。
7,コンポーネントの最終形態
Projectorコンポーネントの Material に5で作成したマテリアルを設定します。
6で作成したスクリプトを追加して完成です。
注意点は、Near Far Orthographic Size を Projector と Cameraで同じ値にすること、Near と Far の差が大きいと誤差が大きくなるので、汚く描画されることです。
8、完成
ライセンス
この作品はユニティちゃんライセンス条項の元に提供されています