Newポケモンスナップ、いろんなフィルタがあって現代の写真遊びに則ったカメラゲームらしさがあって凄く素敵ですね。
いろんなフィルタがある中、シルエットフィルタはゲームらしいフィルタで意外と他のゲームでは見ないような気がするものだったのでいいなぁと思いました。
深度テクスチャを使えば簡単に実現できそうで、入れるだけで結構印象的な写真になるのでちょっと真似してみました。
こんな感じ
そのまんまですね。
空いてる空間にロゴでも入れればデスクトップ壁紙とかに出来そう。
こんな感じ?
この記事とは直接関係ないですが、ロゴの自動生成ツールで面白いサイトがあったのでここで作ってみました↓
実装
ポストエフェクトでカメラの深度テクスチャを取得し、任意の色を乗算して表示しています。
Shader "ShowDepthTexture"
{
SubShader
{
Cull Off
ZTest Always
ZWrite Off
Tags { "RenderType"="Opaque" }
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;
};
sampler2D _CameraDepthTexture;
float _Start;
float _End;
float4 _Color;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 深度テクスチャ
half depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
depth = LinearEyeDepth(depth);
// Start〜Endの距離で濃淡の幅を使う
depth -= _Start;
half max = _End - _Start;
depth = lerp(0, 1, depth / max);
// 色を乗算
return depth * _Color;
}
ENDCG
}
}
}
シェーダーをカメラに反映して、濃淡の範囲と色をInspectorから指定するDepthTexture.cs
を作ります。
using UnityEngine;
public class DepthTexture : MonoBehaviour
{
private static readonly int PROPERTY_COLOR = Shader.PropertyToID("_Color");
private static readonly int PROPERTY_START = Shader.PropertyToID("_Start");
private static readonly int PROPERTY_END = Shader.PropertyToID("_End");
[SerializeField]
private Shader _shader;
private Material _material;
[SerializeField]
private Color _color= Color.white;
[SerializeField]
private float _start = 0f;
[SerializeField]
private float _end = 20f;
void Start()
{
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;
_material = new Material(_shader);
OnValidate();
}
private void OnRenderImage(RenderTexture source, RenderTexture dest)
{
Graphics.Blit(source, dest, _material);
}
private void OnValidate()
{
if(_material == null)
{
return;
}
_material.SetColor(PROPERTY_COLOR, _color);
_material.SetFloat(PROPERTY_START, _start);
_material.SetFloat(PROPERTY_END, _end);
}
}
DepthTexture.cs
コンポーネントをカメラにアタッチして、Shader
メンバにShowDepthTexture.shader
の参照を入れればOKです。
Start
以下の距離だとオブジェクトが真っ黒になり、End
以上の距離だとオブジェクトが真っ白になります。
その範囲のオブジェクトは深度に合わせた濃さの色にColor
で指定した色が乗算されます。
ちょっと改良する
ひとまずこれでいい感じのシルエット写真は作れるようになりましたが、ちょっと困りごとがあります。
Start
とEnd
のパラメータさえ調整すればどうにでもなるんですが、そのぶん被写体との距離に合わせて適宜距離や2値の範囲を調整しないと全体的にコントラストが低く、ぼやっとした絵になってしまいます。
とりあえず被写体(今回だと人物)が風景と比べて十分濃くなっていれば雰囲気が出そうなので、被写体オブジェクトを渡せば対象との距離を見て動的に深度をコントロールしてくれるようなコンポーネントになるよう調整してみます。
half _TargetDistance; // 被写体までの距離
half _TargetThickness; // 被写体の厚み
half _NearMax; // _Start〜被写体間の最大濃度
half _FarMin; // 被写体〜_End間の最小濃度
// ~~~略~~~
fixed4 frag(v2f i) : SV_Target
{
half depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
depth = LinearEyeDepth(depth);
// 被写体の背面までの距離
half back = _TargetDistance + _TargetThickness;
// _Start〜被写体間のdepth値を、0〜_NearMaxの値に調整
half near = lerp(0, _NearMax, (depth - _Start) / (back - _Start));
// 被写体〜_End間のdepth値を、_FarMin〜1の値に調整
half far = lerp(_FarMin, 1, (depth - back) / (_End - back));
// この位置の深度がbackより前か後ろかで反映する値を仕分ける
depth = near * clamp(sign(depth - back), 0, 1) + far * clamp(sign(back - depth), 0, 1);
return depth * _Color;
}
被写体までの距離を閾値として表示する濃度を指定できるようして、被写体と背景のコントラストをコントロールしやすいようにしてみました。
clamp(sign(~~~
の係数は if を使わずにnear
とfar
の値の使い分けをするための雑な計算です(もっといいやり方があるはず…)
コンポーネントもシェーダーに合わせて適宜メンバーと処理を追加します。
private static readonly int PROPERTY_TARGET_DISTANCE = Shader.PropertyToID("_TargetDistance");
private static readonly int PROPERTY_TARGET_THICKNESS = Shader.PropertyToID("_TargetThickness");
private static readonly int PROPERTY_NEAR_MAX = Shader.PropertyToID("_NearMax");
private static readonly int PROPERTY_FAR_MIN = Shader.PropertyToID("_FarMin");
[SerializeField]
private Transform _target;
[SerializeField]
private float _targetThickness = 0.1f;
[SerializeField, Range(0f, 1f)]
private float _nearMaxDepth = 0.2f;
[SerializeField, Range(0f, 1f)]
private float _farMinDepth = 0.6f;
// ~~~略~~~
private void Update()
{
if(_target == null)
{
return;
}
float distance = Vector3.Distance(transform.position, _target.position);
_material.SetFloat(PROPERTY_TARGET_DISTANCE, distance);
}
private void OnValidate()
{
if(_material == null)
{
return;
}
_material.SetColor(PROPERTY_COLOR, _color);
_material.SetFloat(PROPERTY_START, _start);
_material.SetFloat(PROPERTY_END, _end);
_material.SetFloat(PROPERTY_START, _start);
_material.SetFloat(PROPERTY_TARGET_THICKNESS, _targetThickness);
_material.SetFloat(PROPERTY_NEAR_MAX, _nearMaxDepth);
_material.SetFloat(PROPERTY_FAR_MIN, _farMinDepth);
}
Updateで被写体_target
とカメラの距離を計算しています。
設定例
-
Near Max Depth
= 0.1 -
Far Min Depth
= 0.3
となっています。
Target
に被写体として設定しているPlayerから前の表示の濃度は0.1
以下になり、それより後ろは0.3
以上になるため、被写体がくっきりとしたシルエットを表示しやすくなっています。
今回はここまで。