オブジェクト画像の表示が欲しい時にPreviewRenderUtilityという存在を知りました
このPreviewRenderUtilityというクラスはドキュメントすらないクラスのため
今回は備忘録としてUIElementのOverlayを使用してSceneViewのみで完結するオブジェクト配置エディタを作成しました
Unityバージョンは2022 3.22f1です
アタッチするオブジェクトの階層構造について
今回作るオブジェクト配置ツールは
ObjectFieldに入れるPrefabの階層構造は
以下のようになっていないと動作しないようになっています
Prefabのルート
┗配置したいオブジェクト1
┗配置したいオブジェクト2
┗配置したいオブジェクト3...
Overlayの作成
PlacementData
public class PlacementData
{
public string Name { get;private set; }
public Texture2D PreviewImage { get;private set; }
public GameObject Prefab { get;private set; }
public PlacementData(Texture2D texture,GameObject prefab)
{
Name = prefab.name;
PreviewImage = texture;
Prefab = prefab;
}
}
PlacementObjectItem内に必要なデータのクラスです
PlacementObjectView
[Overlay(typeof(SceneView), "配置ツール", true)]
public class PlacementObjectView : Overlay
{
private VisualElement m_Root;
private ScrollView m_ScrollView;
private List<PlacementObjectItem> m_Items = new List<PlacementObjectItem>();
public override VisualElement CreatePanelContent()
{
var root = new VisualElement();
root.style.flexDirection = FlexDirection.Column;
var placementObjectViewToolbar = new PlacementObjectViewToolbar();
root.style.width = 800;
m_ScrollView = new ScrollView();
m_ScrollView.style.marginTop = 10;
placementObjectViewToolbar.OnSelectObject += OnSelectObject;
placementObjectViewToolbar.OnSearchObject += OnSearchObject;
root.Add(placementObjectViewToolbar);
root.Add(m_ScrollView);
m_Root = root;
return m_Root;
}
private void OnSearchObject(string keyword)
{
if( m_Items.Count == 0) return;
if (keyword == "")
{
foreach (var item in m_Items)
{
item.style.display = DisplayStyle.Flex;
}
return;
}
foreach (var item in m_Items)
{
DisplayStyle display = DisplayStyle.Flex;
if (item.name.IndexOf(keyword,StringComparison.OrdinalIgnoreCase) >= 0) display = DisplayStyle.Flex;
else display = DisplayStyle.None;
item.style.display = display;
}
}
private void OnSelectObject(GameObject obj)
{
m_ScrollView.contentContainer.Clear();
m_ScrollView.Clear();
m_Items.Clear();
if (obj == null)
{
m_ScrollView.style.height = Length.Auto();
return;
}
VisualElement itemWrapper = new VisualElement();
itemWrapper.style.flexDirection = FlexDirection.Row;
itemWrapper.style.flexWrap = Wrap.Wrap;
var generator = new PlacementDataGenerator();
PlacementData[] datas = generator.GenerateDatas(obj);
generator.Dispose();
if (datas == null) return;
foreach (PlacementData data in datas)
{
var item = new PlacementObjectItem(data);
m_Items.Add(item);
itemWrapper.Add(item);
}
m_ScrollView.contentContainer.Add(itemWrapper);
m_ScrollView.style.height = 350;
}
}
メインのOverlayです
Toolbar内のObjectFieldにPrefabがアタッチされるとActionを受け取りScrollViewの中を更新します
PlacementObjectViewToolbar
public class PlacementObjectViewToolbar : VisualElement
{
public Action<GameObject> OnSelectObject { get; set; }
public Action<string> OnSearchObject { get; set; }
public PlacementObjectViewToolbar()
{
this.style.marginTop = 5;
this.style.flexDirection = FlexDirection.Row;
this.style.justifyContent = Justify.SpaceBetween;
ObjectField objectField = new ObjectField();
this.Add(objectField);
objectField.objectType = typeof(GameObject);
objectField.RegisterValueChangedCallback(OnObjectRegisterdValue);
objectField.UnregisterValueChangedCallback(OnObjectUnRegisterdValue);
ToolbarSearchField searchField = new ToolbarSearchField();
this.Add(searchField);
searchField.RegisterValueChangedCallback(OnSearchFieldRegisterdValue);
searchField.UnregisterValueChangedCallback(OnSearchFieldUnRegisterdValue);
}
private void OnObjectRegisterdValue(ChangeEvent<Object> value)
{
OnSelectObject?.Invoke(value.newValue.GameObject());
}
private void OnObjectUnRegisterdValue(ChangeEvent<Object> value)
{
OnSelectObject?.Invoke(null);
}
private void OnSearchFieldRegisterdValue(ChangeEvent<string> value)
{
OnSearchObject?.Invoke(value.newValue);
}
private void OnSearchFieldUnRegisterdValue(ChangeEvent<string> value)
{
OnSearchObject?.Invoke("");
}
}
ObjectFieldと検索バーをOverlayToolbarでまとめたクラスです
PlacementObjectItem
public class PlacementObjectItem :Button
{
private GameObject m_Prefab;
public PlacementObjectItem(PlacementData data)
{
this.style.width = this.style.height = 100;
m_Prefab = data.Prefab;
this.style.backgroundImage = data.PreviewImage;
Label label = new Label();
label.text = data.Name;
this.Add(label);
this.clicked += OnClicked;
this.name = data.Name;
}
private void OnClicked()
{
Debug.Log($"OnClicked {m_Prefab.name}");
var obj = GameObject.Instantiate(m_Prefab);
obj.name = obj.name.Replace("(Clone)","");
obj.transform.position = SceneView.lastActiveSceneView.camera.transform.forward * 3;
}
}
Buttonとオブジェクト名を入れているだけです
今回はPlacementObjectViewをごちゃごちゃにしたくないのと
Instantiate後クラスで管理する必要は全くないのでこのクラスに押下後の動作も記入しています
PlacementDataGenerator
public class PlacementDataGenerator:IDisposable
{
private const float OBJECT_DISTANCE = 3f;
private const float OBJECT_PREVIEW_SIZE_FACTOR = 0.6f;
private PreviewRenderUtility m_PreviewRenderUtility = new();
public PlacementData[] GenerateDatas(GameObject objects)
{
GameObject parentObject = m_PreviewRenderUtility.InstantiatePrefabInScene(objects);
m_PreviewRenderUtility.AddSingleGO(parentObject);
m_PreviewRenderUtility.camera.clearFlags = CameraClearFlags.Depth;
m_PreviewRenderUtility.camera.orthographic = true;
List<GameObject> childs = new List<GameObject>();
List<PlacementData> placementDatas = new List<PlacementData>();
parentObject.transform.Cast<Transform>().ToList().ForEach(child => child.gameObject.SetActive(false));
for (int i = 0; i < parentObject.transform.childCount; i++)
{
GameObject child = parentObject.transform.GetChild(i).gameObject;
Texture2D image = CaptureObjectImage(child);
placementDatas.Add(new PlacementData(image,objects.transform.GetChild(i).gameObject));
}
return placementDatas.ToArray();
}
private Texture2D CaptureObjectImage(GameObject child)
{
child.SetActive(true);
Bounds bounds = GetRenderersBounds(child);
//NOTE:child.transform.rightをしているのは今回使用しているアセットの大半がright方向を向いていたためです
//本来はforwardにしています
m_PreviewRenderUtility.camera.transform.position = bounds.center + child.transform.right * OBJECT_DISTANCE;
m_PreviewRenderUtility.camera.transform.LookAt(bounds.center);
m_PreviewRenderUtility.camera.orthographicSize = bounds.size.magnitude / (OBJECT_DISTANCE * OBJECT_PREVIEW_SIZE_FACTOR);
m_PreviewRenderUtility.BeginStaticPreview(new Rect(0, 0, 400, 400));
m_PreviewRenderUtility.Render();
child.SetActive(false);
return m_PreviewRenderUtility.EndStaticPreview();
}
private Bounds GetRenderersBounds(GameObject child)
{
Renderer[] renderers = child.GetComponentsInChildren<Renderer>();
bool first = true;
Bounds totalBounds = new Bounds();
foreach (var renderer in renderers)
{
if (first)
{
totalBounds = renderer.bounds;
first = false;
continue;
}
totalBounds.Encapsulate(renderer.bounds);
}
return totalBounds;
}
public void Dispose()
{
m_PreviewRenderUtility.Cleanup();
}
}
入力されたオブジェクトからPlacementDataを作成するためのクラスです
PreviewRenderUtilityはこのクラスで使用されています
PreviewRenderUtilityについての解説
以下のリンクでソースの中身は見れます
今回具体的に実装したのはプレビュー画像の生成部分でした
解説していきます
GameObject parentObject = m_PreviewRenderUtility.InstantiatePrefabInScene(objects);
m_PreviewRenderUtility.AddSingleGO(parentObject);
m_PreviewRenderUtility.camera.clearFlags = CameraClearFlags.Depth;
m_PreviewRenderUtility.camera.orthographic = true;
まず入力されたオブジェクトをインスタンス化します
Editorでの生成ですのでInstantiatePrefabInSceneを使用してインスタンス化し
AddSignalGOで入れてあげるとシーンにオブジェクトが追加されます
PreviewRenderUtilityではcameraが取得できるようなのでカメラの調整を行います
private Bounds GetRenderersBounds(GameObject child)
{
Renderer[] renderers = child.GetComponentsInChildren<Renderer>();
bool first = true;
Bounds totalBounds = new Bounds();
foreach (var renderer in renderers)
{
if (first)
{
totalBounds = renderer.bounds;
first = false;
continue;
}
totalBounds.Encapsulate(renderer.bounds);
}
return totalBounds;
}
GetRenderersBounds関数内は使用したアセットの家具が細かいパーツに分かれていたので
子にある全てのRendererを取得しBoundsの合成をしてBoundsとして返すような感じです
こうすることで子階層にあるパーツをすべて含んだ中央の位置を取得できます
private Texture2D CaptureObjectImage(GameObject child)
{
child.SetActive(true);
Bunds bounds = GetRenderersBounds(child);
m_PreviewRenderUtility.camera.transform.position = bounds.center + child.transform.right * OBJECT_DISTANCE;
m_PreviewRenderUtility.camera.transform.LookAt(bounds.center);
m_PreviewRenderUtility.camera.orthographicSize = bounds.size.magnitude / (OBJECT_DISTANCE * OBJECT_PREVIEW_SIZE_FACTOR);
m_PreviewRenderUtility.BeginStaticPreview(new Rect(0, 0, 400, 400));
m_PreviewRenderUtility.Render();
child.SetActive(false);
return m_PreviewRenderUtility.EndStaticPreview();
}o
ここがメインの撮影処理です
カメラをオブジェクトのforward方向(今回は正面がrightだったのでright方向)に移動させて
LookAtでBoundsの中央の方向を向かせています
orthographicSizeは bounds.size.magnitudeとカメラとオブジェクトとの距離を割ってあげるとぴったりのサイズになるので
良い感じに倍率かけて調整してあげてください
BeginStaticPreviewとRenderを実行した後
EndStaticPreviewの帰り値でTexture2Dが返ってくるのでそれをオブジェクトの画像として利用しています
最後に使用した後はCleanupという関数を呼び出さないとエラーが出るので呼び出してあげましょう
今回はIDisposableに実装しました
完成
こんな感じで出来上がりました
UIToolkitはコードだけでも完結するのがとても良いところです
配置するオブジェクトが多くてProjectからPrefabを探してHierarchyにドロップするのが面倒な方はぜひ作ってみてください