2023/06/09 : 初稿
Unity : 2021.3.15f1
マシン : M1Mac(Metal)
やりたいこと
窓から差し込む光。レイマーチング?
ポストエフェクトで。
例えば下記のように右に隙間がある室内のシーン。
隙間から光が差し込んでいる時に
今回はとにかく動かすことを目標に。
マルチプラットフォームとか処理負荷、実用性は置いときます。
実現方法
手順は以下の通り。
- 窓の外にある光源から平行投影でDepthをテクスチャにレンダリング。
- メインカメラでポストエフェクト。
ポストエフェクトでの処理はこんな感じ。
- メインカメラからみた最も奥にあるピクセル(ZがDepthになる)をワールド座標に変換
- そこからカメラまでの線分上にある点について、光源から見えているか(光源との間に遮蔽物がないかどうか)をみて、見えているならそこに光の筋があるということでそのピクセルを光らせる。
当然、メインカメラから各ピクセルまでの線分上の全ての点について
光源から見えているかどうかを調べることはできないので、
一定距離間隔などでサンプリングすることになるけど、
このサンプリング数を大きくとらないと綺麗な結果にはならない。
もちろん大きくすればそれだけ重い。
光源から見えているかどうかの判定は、最初に光源からレンダリングしたテクスチャのDepthを使う。
まずは準備として、コールバックするだけの汎用的なRendererFeatureを用意しました。
こいつをメインカメラに使うRenderer(UniversalRendererData)に設定します。
(別にこれ使わなくても実際の処理をRendererFeatureに書けばいいだけですが好みで)
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using System.Collections.Generic;
namespace Utils
{
public class CallbackRendererFeature : ScriptableRendererFeature
{
// コールバック
public interface ICallback
{
void Callback(ScriptableRenderContext context, SharedData data, RenderPassEvent ev);
}
public static List<ICallback> Callbacks { get; private set; } = new List<ICallback>();
// pass
RendererPass BeginRenderPass;
RendererPass EndRenderPass;
// Create
public override void Create()
{
Setup();
}
// OnDestroy
void OnDestroy()
{
Cleanup();
}
// Setup
void Setup()
{
if (Shared == null) {
Shared = new SharedData();
Shared.Setup();
}
if (BeginRenderPass == null) {
BeginRenderPass = new RendererPass();
BeginRenderPass.Setup(Shared, RenderPassEvent.BeforeRendering);
}
if (EndRenderPass == null) {
EndRenderPass = new RendererPass();
EndRenderPass.Setup(Shared, RenderPassEvent.AfterRendering);
}
}
// Cleanup
void Cleanup()
{
if (BeginRenderPass != null) {
BeginRenderPass.Cleanup();
BeginRenderPass = null;
}
if (EndRenderPass != null) {
EndRenderPass.Cleanup();
EndRenderPass = null;
}
if (Shared != null) {
Shared.Cleanup();
Shared = null;
}
}
// OnEnable
void OnEnable()
{
Setup();
}
// OnDisable
void OnDisable()
{
Cleanup();
}
// Dispose
protected override void Dispose(bool disposing)
{
// Cleanup
Cleanup();
// base
base.Dispose(disposing);
}
// AddRenderPasses : 毎フレーム呼ばれる
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
// プレイ中のみ
if (!Application.isPlaying) {
return;
}
// パスを登録
if (BeginRenderPass != null) {
renderer.EnqueuePass(BeginRenderPass);
}
if (EndRenderPass != null) {
renderer.EnqueuePass(EndRenderPass);
}
}
// shared
public class SharedData
{
// カメラ
public Camera CurrentCamera;
public bool IsSceneViewCamera;
// オリジナルカラーバッファ
public RenderTargetIdentifier OriginalColorIdentifier;
public RenderTargetIdentifier OriginalDepthIdentifier;
public RenderTextureDescriptor OriginalDesc;
// デストラクタ
~SharedData()
{
Cleanup();
}
// Setup
public void Setup()
{
}
// Cleanup
public void Cleanup()
{
CurrentCamera = null;
}
// カメラの設定
public void SetRenderingData(ref RenderingData renderingData)
{
CurrentCamera = renderingData.cameraData.camera;
IsSceneViewCamera = renderingData.cameraData.isSceneViewCamera;
OriginalDesc = renderingData.cameraData.cameraTargetDescriptor;
OriginalColorIdentifier = renderingData.cameraData.renderer.cameraColorTarget;
OriginalDepthIdentifier = renderingData.cameraData.renderer.cameraDepthTarget;
}
}
SharedData Shared;
// pass
public class RendererPass : ScriptableRenderPass
{
// Setup
bool HasSetup;
// shared
SharedData Shared;
// Setup
public void Setup(SharedData shared, RenderPassEvent ev)
{
// null check
if (shared == null) {
return;
}
// store
Shared = shared;
renderPassEvent = ev;
// Setupしました
HasSetup = true;
}
// Cleanup
public void Cleanup()
{
HasSetup = false;
}
// Execute
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
// check
if (!HasSetup) {
return;
}
// shared
Shared.SetRenderingData(ref renderingData);
// シーンビューはおかしい?
if (Shared.IsSceneViewCamera) {
return;
}
// コールバック呼び出し
Callbacks.RemoveAll((c) => c == null);
foreach (var callback in Callbacks) {
if (callback == null) {
continue;
}
callback.Callback(context, Shared, renderPassEvent);
}
}
}
}
}
これを使って、光源から見たDepthをテクスチャにして、メインカメラ描画後にポストエフェクトをかけます。
下記のスクリプトを、窓の外に置いた光源のGameObjectあたりにAdd。このGameObjectは室内を見るよう、位置と向きを設定しておきます。
スクリプトは、動的に光源から見たDepthをテクスチャに描画するカメラを生成し、メインカメラより先に処理させます。さらに、メインカメラ描画後に専用シェーダーで画面全体に一枚のポリゴンを描いて、その中で光の筋を加算描画します。
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEditor;
namespace Utils
{
public class RayMarchingManager : BaseBehaviour, CallbackRendererFeature.ICallback
{
// inspector
[SerializeField] bool AutoStart;
[SerializeField] int RenderTextureSize = 256;
[SerializeField] Shader MainShader;
[SerializeField] Shader AddShader;
[SerializeField] int AddDiv = 16;
[SerializeField] int RayRendererIndex;
[SerializeField] float RayCameraOrthoSize = 20;
[SerializeField] float RayCameraNear = 0.3f;
[SerializeField] float RayCameraFar = 20;
[SerializeField] int SamplingCount = 20;
[SerializeField] float SamplingDisMin = 0.3f;
[SerializeField] float SamplingDisMax = 20;
[SerializeField] float SamplingDisOffset = 0.01f;
[SerializeField] float AlphaScale = 0.1f;
[SerializeField] Color Color = Color.white;
// MainCamera
Camera MainCamera;
// OrthoCamera
protected Camera OrthoCamera;
RenderTexture RenderTexture;
// ポストエフェクト
protected Material MainMaterial;
Material AddMaterial;
Mesh Mesh;
// propId
protected int PropId_SamplingCount;
protected int PropId_SamplingDisOffset;
protected int PropId_SamplingDisMin;
protected int PropId_SamplingDisMax;
protected int PropId_AlphaScale;
protected int PropId_Color;
protected int PropId_RayMarchingDepth;
protected int PropId_MainCameraWorldPos;
protected int PropId_MainCameraWorldDir;
protected int PropId_MainCameraWorldUp;
protected int PropId_MainCameraWorldRight;
protected int PropId_RayCameraWorldToLocal;
protected int PropId_RayCameraParam;
// 縮小バッファ
int TempTexId;
// Start
protected virtual void Start()
{
if (AutoStart) {
Setup(Camera.main);
}
}
// OnDestroy
protected virtual void OnDestroy()
{
// 解除
CallbackRendererFeature.Callbacks.Remove(this);
// RenderTexture破棄
RenderTexture = Tools.DestroyObject(RenderTexture);
AddMaterial = Tools.DestroyObject(AddMaterial);
MainMaterial = Tools.DestroyObject(MainMaterial);
Mesh = Tools.DestroyObject(Mesh);
// 参照カット
MainCamera = null;
}
// Setup
public virtual void Setup(Camera mainCamera)
{
// store
MainCamera = mainCamera;
// propIds
PropId_SamplingCount = Shader.PropertyToID("_SamplingCount");
PropId_SamplingDisOffset = Shader.PropertyToID("_SamplingDisOffset");
PropId_SamplingDisMin = Shader.PropertyToID("_SamplingDisMin");
PropId_SamplingDisMax = Shader.PropertyToID("_SamplingDisMax");
PropId_AlphaScale = Shader.PropertyToID("_AlphaScale");
PropId_Color = Shader.PropertyToID("_Color");
PropId_RayMarchingDepth = Shader.PropertyToID("_RayMarchingDepth");
PropId_MainCameraWorldPos = Shader.PropertyToID("_MainCameraWorldPos");
PropId_MainCameraWorldDir = Shader.PropertyToID("_MainCameraWorldDir");
PropId_MainCameraWorldUp = Shader.PropertyToID("_MainCameraWorldUp");
PropId_MainCameraWorldRight = Shader.PropertyToID("_MainCameraWorldRight");
PropId_RayCameraWorldToLocal = Shader.PropertyToID("_RayCameraWorldToLocal");
PropId_RayCameraParam = Shader.PropertyToID("_RayCameraParam");
// 光源からのデプスを描画するレンダーテクスチャ
RenderTexture = new RenderTexture(RenderTextureSize, RenderTextureSize, 24, RenderTextureFormat.Depth);
RenderTexture.wrapMode = TextureWrapMode.Clamp;
//RenderTexture.filterMode = FilterMode.Point;
// ポストエフェクト用メッシュとマテリアル
MainMaterial = new Material(MainShader);
ApplyInspectorParam();
AddMaterial = new Material(AddShader);
Mesh = CreateMesh();
// 光源カメラを作成
var go = new GameObject("RayCamera");
go.transform.SetParent(transform);
go.transform.SetLocalPositionAndRotation(default, default);
go.transform.localScale = Vector3.one;
OrthoCamera = go.AddComponent<Camera>();
OrthoCamera.orthographic = true;
OrthoCamera.orthographicSize = RayCameraOrthoSize;
OrthoCamera.nearClipPlane = RayCameraNear;
OrthoCamera.farClipPlane = RayCameraFar;
OrthoCamera.depth = MainCamera.depth - 1;
OrthoCamera.cullingMask = mainCamera.cullingMask;
OrthoCamera.clearFlags = CameraClearFlags.SolidColor;
OrthoCamera.backgroundColor = Color.clear;
OrthoCamera.targetTexture = RenderTexture;
OrthoCamera.depthTextureMode = DepthTextureMode.Depth;
// camData
var camData = go.AddComponent<UniversalAdditionalCameraData>();
camData.SetRenderer(RayRendererIndex);
camData.renderShadows = false;
// 縮小バッファ
TempTexId = Shader.PropertyToID("_TempTex");
// callback
CallbackRendererFeature.Callbacks.Add(this);
}
// SetColor
public void SetColor(Color color)
{
if (MainMaterial == null) {
return;
}
MainMaterial.SetColor(PropId_Color, color);
}
// SetAlphaScale
public void SetAlphaScale(float value)
{
if (MainMaterial == null) {
return;
}
MainMaterial.SetFloat(PropId_AlphaScale, value);
}
// ApplyInspectorParam
void ApplyInspectorParam()
{
MainMaterial.SetFloat(PropId_SamplingCount, SamplingCount);
MainMaterial.SetFloat(PropId_SamplingDisMin, Mathf.Max(SamplingDisMin, MainCamera.nearClipPlane));
MainMaterial.SetFloat(PropId_SamplingDisMax, Mathf.Min(SamplingDisMax, MainCamera.farClipPlane));
MainMaterial.SetFloat(PropId_SamplingDisOffset, SamplingDisOffset);
MainMaterial.SetFloat(PropId_AlphaScale, AlphaScale);
MainMaterial.SetColor(PropId_Color, Color);
}
// CreateMesh
Mesh CreateMesh()
{
// mesh
var mesh = new Mesh();
mesh.name = "RayMarchingMesh";
// 頂点
Vector3[] vertices = new Vector3[]
{
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(0, 1, 0),
new Vector3(1, 1, 0),
};
Vector2[] uvs = new Vector2[]
{
new Vector2(0, 0),
new Vector2(1, 0),
new Vector2(0, 1),
new Vector2(1, 1),
};
int[] tris = new int[]
{
0, 2, 1,
1, 2, 3,
};
// メッシュ構築
mesh.vertices = vertices;
mesh.uv = uvs;
mesh.triangles = tris;
mesh.RecalculateNormals();
mesh.RecalculateTangents();
mesh.RecalculateBounds();
return mesh;
}
// Callback
public virtual void Callback(ScriptableRenderContext context, CallbackRendererFeature.SharedData data, RenderPassEvent ev)
{
// check
if (!isActiveAndEnabled) {
return;
}
// メインカメラでのポストエフェクト
// 各ピクセルからzがdepthバッファになるワールド座標を計算。
// カメラからその座標までの間を何点かサンプリングして、
// その座標が光源から見えているなら光の筋として光らせる
if (ev == RenderPassEvent.AfterRendering && data.CurrentCamera == MainCamera) {
// コマンドバッファを取得
CommandBuffer cmd = CommandBufferPool.Get("RayMarchingMain");
// set matrix
cmd.SetViewMatrix(Matrix4x4.identity);
cmd.SetViewProjectionMatrices(Matrix4x4.identity, Matrix4x4.Ortho(0, 1, 0, 1, -100, 100));
// 光源から見たデプスを一時テクスチャに
MainMaterial.SetTexture(PropId_RayMarchingDepth, RenderTexture);
// メインカメラ情報
var mainCamTrans = MainCamera.transform;
Vector4 mainCamWorldPos = mainCamTrans.position;
mainCamWorldPos.w = MainCamera.nearClipPlane;
MainMaterial.SetVector(PropId_MainCameraWorldPos, mainCamWorldPos);
Vector4 mainCamWorldDir = mainCamTrans.forward;
mainCamWorldDir.w = MainCamera.farClipPlane;
MainMaterial.SetVector(PropId_MainCameraWorldDir, mainCamWorldDir);
var fovy = MainCamera.fieldOfView;
var tanY = Mathf.Tan(Mathf.Deg2Rad * fovy * 0.5f);
var tanX = tanY * data.OriginalDesc.width / data.OriginalDesc.height;
Vector3 mainCamWorldUp = mainCamTrans.up * tanY;
Vector3 mainCamWorldRight = mainCamTrans.right * tanX;
MainMaterial.SetVector(PropId_MainCameraWorldUp, mainCamWorldUp);
MainMaterial.SetVector(PropId_MainCameraWorldRight, mainCamWorldRight);
// 光源情報
MainMaterial.SetMatrix(PropId_RayCameraWorldToLocal, OrthoCamera.worldToCameraMatrix);
var orthoY = OrthoCamera.orthographicSize;
var orthoX = orthoY;
Vector4 rayCameraParam = new Vector4(OrthoCamera.nearClipPlane, OrthoCamera.farClipPlane, 1.0f / orthoX, 1.0f / orthoY);
MainMaterial.SetVector(PropId_RayCameraParam, rayCameraParam);
// 一時テクスチャ生成
var desc = data.OriginalDesc;
desc.width = desc.width / AddDiv;
desc.height = desc.height / AddDiv;
cmd.GetTemporaryRT(TempTexId, desc, FilterMode.Bilinear);
// レンダリング先をそちらへ
cmd.SetRenderTarget(TempTexId);
cmd.ClearRenderTarget(false, true, Color.clear);
// 画面全体の描画
cmd.DrawMesh(Mesh, Matrix4x4.identity, MainMaterial);
// フレームバッファに合成
cmd.SetRenderTarget(data.OriginalColorIdentifier);
cmd.Blit(TempTexId, data.OriginalColorIdentifier, AddMaterial);
// restore matrix
cmd.SetViewMatrix(data.CurrentCamera.worldToCameraMatrix);
cmd.SetProjectionMatrix(data.CurrentCamera.projectionMatrix);
// CommandBuffer実行
context.ExecuteCommandBuffer(cmd);
// 一時テクスチャ解放
cmd.ReleaseTemporaryRT(TempTexId);
// CommandBuffer解放
CommandBufferPool.Release(cmd);
}
}
#if UNITY_EDITOR
[CustomEditor(typeof(RayMarchingManager))]
protected class RayMarchingManagerEditor : Editor
{
// OnInspectorGUI
public override void OnInspectorGUI()
{
// base
base.OnInspectorGUI();
// apply
if (GUILayout.Button("Apply")) {
var main = target as RayMarchingManager;
if (main != null) {
main.ApplyInspectorParam();
}
}
}
}
#endif
}
}
RendererIndexにはメインカメラ用ではない(CallbackRendererFeatureを適用していない)レンダラーを用意して指定します。
で、肝心のポストエフェクト用シェーダーはこんな感じ。
Shader "Utils/RayMarching/Main"
{
Properties
{
_SamplingCount("SamplingCount", Float) = 100
_SamplingDisOffset("SamplingDisOffset", Float) = 0.01
_SamplingDisMin("SamplingDisMin", Float) = 1
_SamplingDisMax("SamplingDisMax", Float) = 20
_AlphaScale("AlphaScale", Float) = 0.1
_Color("Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
ZTest Always
ZWrite Off
Blend SrcAlpha One
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
TEXTURE2D(_RayMarchingDepth);
SAMPLER(sampler_RayMarchingDepth);
// メインカメラ
float4 _MainCameraWorldPos; // wはnearまでの距離
float4 _MainCameraWorldDir; // normalized。wはfarまでの距離
float4 _MainCameraWorldUp; // up vector * tanY
float4 _MainCameraWorldRight; // right vector * tanX
// 光源カメラ
float4x4 _RayCameraWorldToLocal;
float4 _RayCameraParam; // x:near y:far z:1/orhtoX w:1/orthoY
// サンプリング
float _SamplingCount;
float _SamplingDisOffset;
float _SamplingDisMax;
float _SamplingDisMin;
// 光の濃さ
float _AlphaScale;
float4 _Color;
v2f vert (appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex.xyz);
o.uv = v.uv;//TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
// サンプリング位置が光源カメラから見えているか
float3 CalcRay(float3 worldPos)
{
// その座標を光源カメラから見たUV
float3 localPos = mul(_RayCameraWorldToLocal, float4(worldPos, 1)).xyz;
float2 sampleUV = float2(localPos.x * _RayCameraParam.z, localPos.y * _RayCameraParam.w);
sampleUV.x = sampleUV.x * 0.5 + 0.5;
sampleUV.y = sampleUV.y * 0.5 + 0.5;
if (sampleUV.x < 0 | sampleUV.x > 1 || sampleUV.y < 0 || sampleUV.y > 1) {
return float3(0, 0, 0);
}
// 光源カメラからのZ距離
float sampleRayDis = -localPos.z;
if (sampleRayDis < _RayCameraParam.x || sampleRayDis > _RayCameraParam.y) {
return float3(0, 0, 0);
}
// 光源カメラのdepth : 平行投影なのでLinear01Depthは使えないというか、使わなくてもリニア。
// ループ内部なのでSAMPLE_DEPTH_TEXTUREを使うとwarningが出る
float rayDepth = SAMPLE_DEPTH_TEXTURE_LOD(_RayMarchingDepth, sampler_RayMarchingDepth, sampleUV, 0);
#if defined(UNITY_REVERSED_Z)
rayDepth = 1 - rayDepth;
#endif
// 光源カメラからこの場所の方向にレイを飛ばして何かにぶつかるまでの距離
float rayDis = (_RayCameraParam.y - _RayCameraParam.x) * rayDepth + _RayCameraParam.x;
// サンプリング位置は光源カメラから見えているか?
float visible = (sampleRayDis > rayDis - _SamplingDisOffset) ? 0 : 1;
return float3(visible, rayDis, sampleRayDis);
}
float4 frag (v2f i) : SV_Target
{
// メインカメラのdepth : Liner01/EyeDepthはnear-farではなく0-far
float mainZDis = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.uv), _ZBufferParams);
// それをメインカメラからみたZ距離とするワールド座標に変換
float3 worldPos = _MainCameraWorldPos.xyz + _MainCameraWorldDir.xyz * mainZDis;
worldPos += _MainCameraWorldUp.xyz * mainZDis * (i.uv.y * 2 - 1);
worldPos += _MainCameraWorldRight.xyz * mainZDis * (i.uv.x * 2 - 1);
// メインカメラからこの場所へ向かうカメラZ方向距離を1とするベクトル
float3 vec = worldPos - _MainCameraWorldPos.xyz;
vec /= mainZDis;
// メインカメラからこの場所までの間の座標をいくつかサンプリングして光のかかり度合いを決める
float a = 0;
float addDis = (_SamplingDisMax - _SamplingDisMin) / _SamplingCount;
[loop]
for (float l = 0; l < _SamplingCount + 1; l++) {
// サンプリングするワールド座標 : メインカメラのnearからスタートしてmaxDisを越えないところまで
float dis = addDis * l + _SamplingDisMin;
if (dis > mainZDis) {
break;
}
float3 sampleWorldPos = _MainCameraWorldPos.xyz + vec * dis;
// そこは光源カメラから見えているか・
float3 r = CalcRay(sampleWorldPos);
// 見えていれば光らせる
a += r.x;
}
a *= _AlphaScale * _Color.a;
a = max(a, 0);
a = min(a, 1);
return float4(_Color.r, _Color.g, _Color.b, a);
}
ENDHLSL
}
}
}
Shader "Utils/Add"
{
Properties
{
[MainTexture] _MainTex ("MainTex", 2D) = "black" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
ZTest Always
ZWrite Off
Blend One One
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex.xyz);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
float4 frag (v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDHLSL
}
}
}
これらのシェーダーはRayMarchingManager.csのインスペクタでMainShader/AddShaderにセットしておきます。なお、Utils/Addは画面に加算合成するだけなので、ガウシアンブラーかけるなりなんなりお好きに。
とりあえずこれで冒頭のスクショのように動きました。
一応光の筋をもっと小さいバッファに描画してぼかしながら画面に合成したけど
とても実用的な速度が出るとは思えない・・・。
光源からなら描画しなくてもシャドウマップ使えるんじゃないかとか、
Windowsマシンで動かないんだけどとか多分色々あるでしょうけど、
最初の一歩としてということでご容赦を。
RendererFeature使わなくてもOnEndCameraRenderingとかのコールバックでもできるのか?
追記。コンパイルに足りないコードがあったので念の為。
namespace Utils
{
public class BaseBehaviour : MonoBehaviour
{
// Transform
Transform _ThisTransform;
public Transform ThisTransform
{
get
{
if (_ThisTransform == null) {
_ThisTransform = transform;
}
return _ThisTransform;
}
}
// RectTransform
RectTransform _ThisRectTransform;
public RectTransform ThisRectTransform
{
get
{
if (_ThisRectTransform == null) {
_ThisRectTransform = GetComponent<RectTransform>();
}
return _ThisRectTransform;
}
}
}
// Tools
public class Tools
{
// DestroyObject
public static T DestroyObject<T>(T obj) where T : UnityEngine.Object
{
if (obj == null) {
return null;
}
if (Application.isPlaying) {
UnityEngine.Object.Destroy(obj);
} else {
UnityEngine.Object.DestroyImmediate(obj);
}
return null;
}
}
}