Skip to content

Instantly share code, notes, and snippets.

@AdPetrou
Forked from katas94/WorldSpaceUIDocument.cs
Last active January 26, 2023 13:56
Show Gist options
  • Save AdPetrou/31ee05e6df40791c638004a42901af91 to your computer and use it in GitHub Desktop.
Save AdPetrou/31ee05e6df40791c638004a42901af91 to your computer and use it in GitHub Desktop.
Custom Unity component to create a world-space UIToolkit panel
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
namespace UnityEngine.UIElements
{
public class WorldSpaceUIDocument : MonoBehaviour, IPointerMoveHandler, IPointerUpHandler, IPointerDownHandler,
ISubmitHandler, ICancelHandler, IMoveHandler, IScrollHandler, ISelectHandler, IDeselectHandler, IDragHandler
{
[Tooltip("Width of the panel in pixels. The RenderTexture used to render the panel will have this width.")]
[SerializeField] protected int _panelWidth = 1280;
[Tooltip("Height of the panel in pixels. The RenderTexture used to render the panel will have this height.")]
[SerializeField] protected int _panelHeight = 720;
[Tooltip("Scale of the panel. It is like the zoom in a browser.")]
[SerializeField] protected float _panelScale = 1.0f;
[Tooltip("Pixels per world units, it will the termine the real panel size in the world based on panel pixel width and height.")]
[SerializeField] protected float _pixelsPerUnit = 1280.0f;
[Tooltip("Visual tree element object of this panel.")]
[SerializeField] protected VisualTreeAsset _visualTreeAsset;
[Tooltip("PanelSettings that will be used to create a new instance for this panel.")]
[SerializeField] protected PanelSettings _panelSettingsPrefab;
[Tooltip("RenderTexture that will be used to create a new instance for this panel.")]
[SerializeField] protected RenderTexture _renderTexturePrefab;
[Tooltip("Some input modules (like the XRUIInputModule from the XR Interaction toolkit package) doesn't send PointerMove events. If you are using such an input module, just set this to true so at least you can properly drag things around.")]
public bool UseDragEventFix = false;
public Vector2 PanelSize
{
get => new Vector2(_panelWidth, _panelHeight);
set
{
_panelWidth = Mathf.RoundToInt(value.x);
_panelHeight = Mathf.RoundToInt(value.y);
RefreshPanelSize();
}
}
public float PanelScale
{
get => _panelScale;
set
{
_panelScale = value;
if (_panelSettings != null)
_panelSettings.scale = value;
}
}
public VisualTreeAsset VisualTreeAsset
{
get => _visualTreeAsset;
set
{
_visualTreeAsset = value;
if (_uiDocument != null)
_uiDocument.visualTreeAsset = value;
}
}
public VisualElement UIWidget
{ get { return _uiDocument.rootVisualElement[0]; } }
public int PanelWidth { get => _panelWidth; set { _panelWidth = value; RefreshPanelSize(); } }
public int PanelHeight { get => _panelHeight; set { _panelHeight = value; RefreshPanelSize(); } }
public float PixelsPerUnit { get => _pixelsPerUnit; set { _pixelsPerUnit = value; RefreshPanelSize(); } }
public PanelSettings PanelSettingsPrefab { get => _panelSettingsPrefab; set { _panelSettingsPrefab = value; RebuildPanel(); } }
public RenderTexture RenderTexturePrefab { get => _renderTexturePrefab; set { _renderTexturePrefab = value; RebuildPanel(); } }
protected MeshRenderer _meshRenderer;
protected PanelEventHandler _panelEventHandler;
// runtime rebuildable stuff
protected UIDocument _uiDocument;
protected PanelSettings _panelSettings;
protected RenderTexture _renderTexture;
protected Material _material;
void Awake()
{
PixelsPerUnit = _pixelsPerUnit;
// dynamically a MeshFilter, MeshRenderer and BoxCollider
MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
_meshRenderer = gameObject.AddComponent<MeshRenderer>();
_meshRenderer.sharedMaterial = null;
_meshRenderer.shadowCastingMode = ShadowCastingMode.Off;
_meshRenderer.receiveShadows = false;
_meshRenderer.allowOcclusionWhenDynamic = false;
_meshRenderer.lightProbeUsage = LightProbeUsage.Off;
_meshRenderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
_meshRenderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
BoxCollider boxCollider = gameObject.AddComponent<BoxCollider>();
Vector3 size = boxCollider.size;
size.z = 0;
boxCollider.size = size;
// set the primitive quad mesh to the mesh filter
GameObject quadGo = GameObject.CreatePrimitive(PrimitiveType.Quad);
meshFilter.sharedMesh = quadGo.GetComponent<MeshFilter>().sharedMesh;
Destroy(quadGo);
}
private bool _isSet = false;
void Start()
{
/*
For some reason, this is getting called multiple times, _isSet stops that from happening, idk why
If this isn't here then it attempts to add the UiDocument to the gameobject multiple times
which results in no return value from adding the component which throws an object reference error
because it can't add the panel to the UiDocument
*/
if (!_isSet)
RebuildPanel();
}
/// <summary>
/// Use this method to initialise the panel without triggering a rebuild (i.e.: when instantiating it from scripts). Start method
/// will always trigger RebuildPanel(), but if you are calling this after the GameObject started you must call RebuildPanel() so the
/// changes take effect.
/// </summary>
public void InitPanel(int panelWidth, int panelHeight, float panelScale, float pixelsPerUnit, VisualTreeAsset visualTreeAsset, PanelSettings panelSettingsPrefab, RenderTexture renderTexturePrefab)
{
_panelWidth = panelWidth;
_panelHeight = panelHeight;
_panelScale = panelScale;
_pixelsPerUnit = pixelsPerUnit;
_visualTreeAsset = visualTreeAsset;
_panelSettingsPrefab = panelSettingsPrefab;
_renderTexturePrefab = renderTexturePrefab;
}
/// <summary>
/// Rebuilds the panel by destroy current assets and generating new ones based on the configuration.
/// </summary>
public void RebuildPanel()
{
_isSet = true;
DestroyGeneratedAssets();
// generate render texture
RenderTextureDescriptor textureDescriptor = _renderTexturePrefab.descriptor;
textureDescriptor.width = _panelWidth;
textureDescriptor.height = _panelHeight;
_renderTexture = new RenderTexture(textureDescriptor);
// generate panel settings
_panelSettings = Instantiate(_panelSettingsPrefab);
_panelSettings.targetTexture = _renderTexture;
_panelSettings.clearColor = true; // ConstantPixelSize and clearColor are mandatory configs
_panelSettings.scaleMode = PanelScaleMode.ConstantPixelSize;
_panelSettings.scale = _panelScale;
_renderTexture.name = $"{name} - RenderTexture";
_panelSettings.name = $"{name} - PanelSettings";
// generate UIDocument
_uiDocument = gameObject.AddComponent<UIDocument>();
_uiDocument.panelSettings = _panelSettings;
_uiDocument.visualTreeAsset = _visualTreeAsset;
// generate material
if (_panelSettings.colorClearValue.a < 1.0f)
_material = new Material(Shader.Find("Unlit/Transparent"));
else
_material = new Material(Shader.Find("Unlit/Texture"));
_material.SetTexture("_MainTex", _renderTexture);
_meshRenderer.sharedMaterial = _material;
RefreshPanelSize();
// find the automatically generated PanelEventHandler and PanelRaycaster for this panel and disable the raycaster
PanelEventHandler[] handlers = FindObjectsOfType<PanelEventHandler>();
foreach (PanelEventHandler handler in handlers)
{
if (handler.panel == _uiDocument.rootVisualElement.panel)
{
_panelEventHandler = handler;
PanelRaycaster panelRaycaster = _panelEventHandler.GetComponent<PanelRaycaster>();
if (panelRaycaster != null)
panelRaycaster.enabled = false;
break;
}
}
}
protected void RefreshPanelSize()
{
if (_renderTexture != null && (_renderTexture.width != _panelWidth || _renderTexture.height != _panelHeight))
{
_renderTexture.Release();
_renderTexture.width = _panelWidth;
_renderTexture.height = _panelHeight;
_renderTexture.Create();
if (_uiDocument != null)
_uiDocument.rootVisualElement?.MarkDirtyRepaint();
}
transform.localScale = new Vector3(_panelWidth / _pixelsPerUnit, _panelHeight / _pixelsPerUnit, 1.0f);
}
protected void DestroyGeneratedAssets()
{
if (_uiDocument) Destroy(_uiDocument);
if (_renderTexture) Destroy(_renderTexture);
if (_panelSettings) Destroy(_panelSettings);
if (_material) Destroy(_material);
}
void OnDestroy()
{
DestroyGeneratedAssets();
}
#if UNITY_EDITOR
void OnValidate ()
{
if (Application.isPlaying && _material != null && _uiDocument != null)
{
if (_uiDocument.visualTreeAsset != _visualTreeAsset)
VisualTreeAsset = _visualTreeAsset;
if (_panelScale != _panelSettings.scale)
_panelSettings.scale = _panelScale;
RefreshPanelSize();
}
}
#endif
///////////////////////// REDIRECTION OF EVENTS TO THE PANEL
protected readonly HashSet<(BaseEventData, int)> _eventsProcessedInThisFrame = new HashSet<(BaseEventData, int)>();
void LateUpdate()
{
_eventsProcessedInThisFrame.Clear();
}
public void OnPointerMove(PointerEventData eventData)
{
TransformPointerEventForUIToolkit(eventData);
_panelEventHandler?.OnPointerMove(eventData);
}
public void OnPointerDown(PointerEventData eventData)
{
TransformPointerEventForUIToolkit(eventData);
_panelEventHandler?.OnPointerDown(eventData);
}
public void OnPointerUp(PointerEventData eventData)
{
TransformPointerEventForUIToolkit(eventData);
_panelEventHandler?.OnPointerUp(eventData);
}
public void OnSubmit(BaseEventData eventData)
{
_panelEventHandler?.OnSubmit(eventData);
}
public void OnCancel(BaseEventData eventData)
{
_panelEventHandler?.OnCancel(eventData);
}
public void OnMove(AxisEventData eventData)
{
_panelEventHandler?.OnMove(eventData);
}
public void OnScroll(PointerEventData eventData)
{
TransformPointerEventForUIToolkit(eventData);
_panelEventHandler?.OnScroll(eventData);
}
public void OnSelect(BaseEventData eventData)
{
_panelEventHandler?.OnSelect(eventData);
}
public void OnDeselect(BaseEventData eventData)
{
_panelEventHandler?.OnDeselect(eventData);
}
public void OnDrag(PointerEventData eventData)
{
if (UseDragEventFix)
OnPointerMove(eventData);
}
protected void TransformPointerEventForUIToolkit(PointerEventData eventData)
{
var eventKey = (eventData, eventData.pointerId);
if (!_eventsProcessedInThisFrame.Contains(eventKey))
{
_eventsProcessedInThisFrame.Add(eventKey);
Camera eventCamera = eventData.enterEventCamera ?? eventData.pressEventCamera;
if (eventCamera != null)
{
// get current event position and create the ray from the event camera
Vector3 position = eventData.position;
position.z = 1.0f;
position = eventCamera.ScreenToWorldPoint(position);
Plane panelPlane = new Plane(transform.forward, transform.position);
Ray ray = new Ray(eventCamera.transform.position, position - eventCamera.transform.position);
if (panelPlane.Raycast(ray, out float distance))
{
// get local pointer position within the panel
position = ray.origin + distance * ray.direction.normalized;
position = transform.InverseTransformPoint(position);
// compute a fake pointer screen position so it results in the proper panel position when projected from the camera by the PanelEventHandler
position.x += 0.5f; position.y -= 0.5f;
position = Vector3.Scale(position, new Vector3(_panelWidth, _panelHeight, 1.0f));
position.y += Screen.height;
// print(new Vector2(position.x, Screen.height - position.y)); // print actual computed position in panel UIToolkit coords
// update the event data with the new calculated position
eventData.position = position;
RaycastResult raycastResult = eventData.pointerCurrentRaycast;
raycastResult.screenPosition = position;
eventData.pointerCurrentRaycast = raycastResult;
raycastResult = eventData.pointerPressRaycast;
raycastResult.screenPosition = position;
eventData.pointerPressRaycast = raycastResult;
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment