今回はUnityで操作キャラクターを写すカメラの前に他のキャラクターや障害物等が入った時に半透明にして操作キャラクターを見えるような機能を作っていきます。
通常であればカメラの前に他のキャラクター等がいると以下のように操作キャラクターが見えなくなってしまいます。
今回の機能を取り付けると以下のようにカメラ前の他のキャラクター等が半透明になって操作キャラクターが見えるようになります。
今回の機能ではUnityのShaderGraphを使用しますので、プロジェクトはURPやHDRP等のスクリタブルレンダーパイプラインである必要があります。
今回使用しているUnityのバージョンは2021.3.7f1でShaderGraphのバージョンは12.1.7を使用しています。
ShaderGraphに関しては以下の記事も参照してください。
他のキャラクター用のシェーダーグラフの作成
まず最初に他のキャラクター用のシェーダーグラフを作成していきます。
今回操作キャラクター、他のキャラクター共にスタンダードアセットのEthanを使用しますが、他のキャラクターでも大丈夫です。
Assetsフォルダ内で右クリックからCreate→Shader Graph→URP→Lit Shader Graphを選択し、名前をOtherCharacterとします。
わたくしの場合はURPを使用していますのでURPのLit Shader Graphを選択していますが、HDRPの場合は適宜変更してください。
OtherCharacterをダブルクリックしてシェーダーグラフのウインドウを開きます。
Blackboardの+を押し、Texture 2Dを選択し、名前をMainTexture、Floatを選択し、名前をAlphaとします。
以下のようにプロパティが出来ました。
MainTextureプロパティをシェーダーグラフウインドウの何もない所にドラッグ&ドロップし、MainTextureのポート部分を右にドラッグ&ドロップして出てきたウインドウの検索窓にsampleと入力し、Sample Texture 2Dノードを選択します。
Sample Texture 2DのRGBAポートをFragmentのBase Colorに接続します。
ここまできたらSave Assetボタンを押して保存します。
他のキャラクター用のマテリアルの作成
他のキャラクター用のマテリアルを作成し、先ほど作ったシェーダーグラフを設定し、キャラクターに設定してみます。
Assetsフォルダ内で右クリックからCreate→Materialを選択し、名前をOtherCharacterとします。
OtherCharacterマテリアルを選択し、インスペクタのShaderからShader Graphs→OtherCharacterを選択します(もしくはOtherCharacterシェーダーグラフをOtherCharacterマテリアルにドラッグ&ドロップ)。
OtherCharacterマテリアルのSurface InputsのMain TextureにEthanOcclusionを設定します(キャラクターのテクスチャ)。
ヒエラルキーにEthanを配置し、名前をOtherCharacterとし、子のEthanBodyとEthanGlassesのSkinnedMeshRendererコンポーネントのMaterialsのElement0にOtherCharacterマテリアルを設定します。
これでキャラクターが通常どおり表示されていればOKです。
実際にはFragmentのNormalにEthanNormalマップを接続していないので通常とは異なっています。
ノーマルマップも設定したい場合は以下のようにノードを接続します。
SampleTexture2DのTypeをNormalにし、TextureにはEthanNormalを設定し、FragmentのNormalに接続します。
今回は指定するTextureをプロパティにしていませんが、プロパティにしておくとインスペクタで設定出来ます。
これでとりあえずOtherCharacterシェーダーを設定したOtherCharacterマテリアルをキャラクターに設定し表示出来る事を確認出来ました。
キャラクターを半透明にする仕組みをシェーダーグラフに追加
キャラクターを通常通り表示出来たので、次は半透明にする仕組みを追加していきます。
OtherCharacterシェーダーグラフウインドウを開きFragmentを選択して、Graph InspectorでAlpha Clippingにチェックを入れます。
チェックを入れるとFragmentにAlphaとAlpha Clip Thresholdが追加されます。
Alphaは透明度で、Alpha Clip Thresholdは指定したアルファ値のしきい値より下の場合は表示しない設定になります。
BlackboardのAlphaプロパティを何もない所にドラッグ&ドロップし、Alphaに接続します。
Alphaプロパティの値を直接Alphaに反映できるようにしました。
Save Assetボタンを押して保存したら、AlphaとAlpha Clip Thresholdの確認をしてみます。
ヒエラルキーのEthanBodyを選択し、インスペクタのマテリアルのSurface InputsのAlphaを0.49にしてみます。
Alphaプロパティを0.49にしたのでシェーダーグラフのAlphaの値はそのまま0.49になりAlpha Clip Thresholdの0.5より値が低くなります。
つまりクリップされてキャラクターが表示されなくなります。
キャラクターが完全に見えなくなると困るのでAlpha Clip Thresholdの値を変更します。
OtherCharacterシェーダーグラフの何もない所でSpaceキーを押して出てきたウインドウの検索窓にDitherと入力し選択します(DitherでなくNoise系でも出来る)。
DitherのInの値を1にし、OutをAlpha Clip Thresholdに接続します。
以前は0.5の値をしきい値としていましたが、Ditherを使う事で所々白~黒の範囲になり、しきい値が変わります。
切り取られた部分があちこちにあるので実際のキャラクターでも所々切り取られ半透明になったかのように見えます(実際には透明ではなく間が切り取られていてそう見える)。
試しにEthanBodyのマテリアルのSurface InputsのAlphaを0.3にして確認してみると以下のように半透明のように見えます。
操作キャラクターの配置
OtherCharacterが出来たので次は自分で操作するキャラクターを配置します。
キャラクターは移動が出来るようにし、カメラはCinemachineのFree Lock Cameraを使って視点の変更が出来るようにしておきます。
キャラクターの移動に関しては以下の記事を参照してください。
Cinemachineに関しては以下の記事を参照してください。
とりあえずキャラクターの移動が出来て、カメラの動きがあれば問題ないです。
ヒエラルキーの操作キャラクターを選択し、インスペクタでTagをPlayerに設定しておきます。
他のキャラクターがカメラに近づいた時にAlpha値を操作する
操作キャラクターが出来たので次にカメラがOtherCharacterキャラクターに近づいた時にOtherCharacterシェーダーグラフのAlphaプロパティの値を操作して半透明にするという処理を作っていきます。
シェーダーグラフ内でカメラとオブジェクトの距離を計算することも出来ますが、今回はC#スクリプトを使って操作することにします。
EraseTheCharacterスクリプトを作成し、ヒエラルキーのOtherCharacterゲームオブジェクトにドラッグ&ドロップして設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class EraseTheCharacter : MonoBehaviour { // 操作キャラクターのTransform private Transform characterTransform; private Renderer[] renderers; private Material[] materials; // オブジェクトのアルファ値を操作するカメラとの距離 [SerializeField] private float cameraDistance = 2f; // カメラとの距離によってアルファ値に影響を与えるオフセット値 [SerializeField] private float alphaOffset = 5f; // 距離を測るカメラ private Camera mainCamera; // カメラ位置と自身の位置の距離 private float distance; // 操作キャラクターとカメラの距離 private float distanceBetweenCharacterAndCamera; // Start is called before the first frame update void Start() { characterTransform = GameObject.FindWithTag("Player").transform; mainCamera = Camera.main; renderers = GetComponentsInChildren<Renderer>(); var i = 0; // Renderer分の配列要素を確保 materials = new Material[renderers.Length]; // Rendererからマテリアルを取得 foreach (var renderer in renderers) { materials[i] = renderer.material; i++; } } // Update is called once per frame void Update() { // カメラ位置と自身の位置の距離を計算 distance = Vector3.Distance(mainCamera.transform.position, transform.position); // 操作キャラクターとカメラの距離 distanceBetweenCharacterAndCamera = Vector3.Distance(mainCamera.transform.position, characterTransform.position); // 操作キャラクターとカメラの間に自身がいる場合、かつcameraDistanceより短い距離にいた場合は距離に応じたアルファ値にする if (distance < distanceBetweenCharacterAndCamera && distance < cameraDistance) { Debug.Log(Mathf.InverseLerp(0f, cameraDistance + alphaOffset, distance)); foreach (var material in materials) { material.SetFloat("_Alpha", Mathf.InverseLerp(0f, cameraDistance + alphaOffset, distance)); } } else { // 指定距離より遠い場合は標準 foreach (var material in materials) { material.SetFloat("_Alpha", 1f); } } } } |
characterTransformは操作キャラクターのTransformです。
renderersは自身の子の全てのレンダラーを入れます。
materialsは自身の子の全てのマテリアルを入れます。
cameraDistanceは自身がカメラとどのくらいの距離に入ったら半透明にするかです。
alphaOffsetはアルファ値に関するオフセット値です。
mainCameraはメインカメラです。
distanceはカメラ位置と自身の距離。
distanceBetweenCharacterAndCameraは操作キャラクターとカメラの距離。
StartメソッドでFindWithTagを使ってPlayerタグを設定された操作キャラクターのTransformを取得しcharacterTransformに入れています。
mainCameraにはCamera.mainを使ってMainCameraを設定されたカメラを取得し入れています。
GetComponentsInChildrenを使って自身の子のRenderer(今回の場合はSkinnedMeshRenderer)を全て取得し、その各マテリアルをmaterialsに入れています。
ここではEthanBodyとEthanGlassesに設定しているマテリアルが同じなので、ループを使って処理をせず、ひとつのマテリアルにRendererの.sharedMaterialプロパティを使って取得し変更するだけで出来ますが、sharedMaterialを使った場合はUnity実行後もマテリアルに加えた変化が反映されてしまうので今回は各materialを使ってmaterialsにそれぞれ入れています。
Updateメソッドでは自身とカメラとの距離を計算しdistanceに入れ、操作キャラクターとカメラの距離をdistanceBetweenCharacterAndCameraに入れています。
自身とカメラの距離が操作キャラクターとカメラの距離よりも短い時は操作キャラクターとカメラの間に自身がいるので半透明にする必要があります。
さらに自身とカメラの距離が指定したcameraDistanceよりも短いという条件も加えます。
条件に合致していたら半透明にする必要があるので各マテリアルの_Alphaの値を設定します。
注意が必要なのが、プロパティの操作にはAlphaプロパティの参照を使用する必要があります。
設定する値はMathf.InverseLerpを使ってdistanceが0~cameraDistance+alphaOffsetの範囲のどこになるかを計算し設定しています。
Mathf.InverseLerpはdistanceが0~cameraDistance+alphaOffsetのどの範囲にあるかを0~1の値で返しますが、範囲外の場合はその限界値の値を返します。
自身とカメラの距離が指定した距離より長い場合は自身を半透明にする必要がないので_Alphaを1にしてクリッピングをしないようにします。
常に計算するのを避ける
先ほどのスクリプトだと毎フレームUpdateメソッドが呼ばれカメラ位置と自身の距離、操作キャラクターとカメラの距離を計算しています。
スクリプトを取り付けたゲームオブジェクトが増えると処理が遅くなる可能性もある為、オブジェクトのレンダラーがカメラの範囲内にいる時だけ距離の計算をするように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class EraseScript : MonoBehaviour { // 操作キャラクターのTransform private Transform characterTransform; private Renderer myRenderer; private Material material; // オブジェクトのアルファ値を操作するカメラとの距離 [SerializeField] private float cameraDistance = 2f; // カメラとの距離によってアルファ値に影響を与えるオフセット値 [SerializeField] private float alphaOffset = 5f; // 距離を測るカメラ private Camera mainCamera; // カメラ位置と自身の位置の距離 private float distance; // 操作キャラクターとカメラの距離 private float distanceBetweenCharacterAndCamera; // レンダラーがカメラの範囲内かどうか private bool isVisible; // Start is called before the first frame update void Start() { characterTransform = GameObject.FindWithTag("Player").transform; mainCamera = Camera.main; myRenderer = GetComponent<Renderer>(); material = myRenderer.material; } // Update is called once per frame void Update() { // カメラの範囲内でなければそれ以降の処理を行わない if (!isVisible) { return; } // カメラ位置と自身の位置の距離を計算 distance = Vector3.Distance(mainCamera.transform.position, transform.position); // 操作キャラクターとカメラの距離 distanceBetweenCharacterAndCamera = Vector3.Distance(mainCamera.transform.position, characterTransform.position); // 操作キャラクターとカメラの間に自身がいる場合、かつcameraDistanceより短い距離にいた場合は距離に応じたアルファ値にする if (distance < distanceBetweenCharacterAndCamera && distance < cameraDistance) { Debug.Log(Mathf.InverseLerp(0f, cameraDistance + alphaOffset, distance)); material.SetFloat("_Alpha", Mathf.InverseLerp(0f, cameraDistance + alphaOffset, distance)); } else { // 指定距離より遠い場合は標準 material.SetFloat("_Alpha", 1f); } } private void OnBecameVisible() { isVisible = true; material.SetFloat("_Alpha", 1f); } private void OnBecameInvisible() { isVisible = false; } } |
isVisibleがカメラの範囲内にレンダラーを持つオブジェクトがいるかいないかを入れます。
UpdateメソッドではそのisVisibleがfalseの時(自身がカメラの範囲外にいる)はreturnでそれ以降の処理をしないようにします。
カメラの範囲内かそうでないかを判断しているのはMonoBehaviourのメソッドのOnBecameVisible(カメラの範囲内にある時に実行される)とOnBecameInVisible(カメラの範囲内を出ると実行される)でしています。
ただオブジェクトがカメラに映っていなくてもOnBecameVisibleが呼ばれることもあります。
ここら辺は以下の記事も参照してみてください。
改変したスクリプトはレンダラーを持つゲームオブジェクト個々に設定するようにする必要があるので(OnBecameVisibleとOnBecameInVisibleはスクリプトが取り付けられたレンダラーで呼び出される為)、今回の場合はOtherCharacterのEraseTheCharacterスクリプトを削除し、子のEthanBodyとEthanGlassesの2つにEraseScriptを取り付けて使用する必要があります。
オブジェクトのレンダラーがカメラの範囲内かどうかで判断する他に、コライダを使って操作キャラクターがコライダの範囲内に入っているかどうかで計算処理をするということも出来ます。
機能の確認
これで機能が出来ました。
今回は他のキャラクター用のマテリアルとしてOtherCharacterを作りましたが、確認の為にヒエラルキーにCubeを配置しOtherCharacterマテリアルを設定し、EraseTheCharacterスクリプト(もしくはEraseScript)を取り付けて障害物も同じように出来る事を確認します。
OtherCharacterの子のEthanBodyを選択し、マテリアルのSurface InputsのAlphaを1に戻しておきます。
終わりに
オブジェクトを半透明にすると影もクリッピングされてしまいます。
これに関しては以下の動画の4:05辺りにあるカスタム関数を使用してAlphaを設定すると影の変更はされません。
今回の機能はPS4のニーアレプリカントにあったので作ってみました。