/******************************************************************************
* Copyright (C) Ultraleap, Inc. 2011-2021. *
* *
* Use subject to the terms of the Apache License 2.0 available at *
* http://www.apache.org/licenses/LICENSE-2.0, or another agreement *
* between Ultraleap and you, your company or other organization. *
******************************************************************************/
using Leap.Unity.Query;
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Serialization;
namespace Leap.Unity
{
///
/// Acquires images from a LeapServiceProvider and uploads image data as shader global
/// data for use by any shaders that render those images.
///
/// Note: To use the LeapImageRetriever, you must be on version 2.1 or newer and you
/// must enable "Allow Images" in your Leap Motion settings.
///
public class LeapImageRetriever : MonoBehaviour
{
public const string GLOBAL_COLOR_SPACE_GAMMA_NAME = "_LeapGlobalColorSpaceGamma";
public const string GLOBAL_GAMMA_CORRECTION_EXPONENT_NAME = "_LeapGlobalGammaCorrectionExponent";
public const string GLOBAL_CAMERA_PROJECTION_NAME = "_LeapGlobalProjection";
public const int IMAGE_WARNING_WAIT = 10;
public const int LEFT_IMAGE_INDEX = 0;
public const int RIGHT_IMAGE_INDEX = 1;
public const float IMAGE_SETTING_POLL_RATE = 2.0f;
[SerializeField]
[FormerlySerializedAs("gammaCorrection")]
private float _gammaCorrection = 1.0f;
[SerializeField] private LeapServiceProvider _provider;
private EyeTextureData _eyeTextureData = new EyeTextureData();
//Image that we have requested from the service. Are requested in Update and retrieved in OnPreRender
protected ProduceConsumeBuffer _imageQueue = new ProduceConsumeBuffer(128);
protected Image _currentImage = null;
private long _prevSequenceId;
private bool _needQueueReset;
// If image IDs from the libtrack server do not reset with the Visualiser, it triggers out-of-sequence
// checks and we lose images. Detecting this and setting an offset allows us to compensate.
private long _frameIDOffset = -1;
public EyeTextureData TextureData
{
get
{
return _eyeTextureData;
}
}
public class LeapTextureData
{
private Texture2D _combinedTexture = null;
private byte[] _intermediateArray = null;
public Texture2D CombinedTexture
{
get
{
return _combinedTexture;
}
}
public bool CheckStale(Image image)
{
if (_combinedTexture == null || _intermediateArray == null)
{
return true;
}
if (image.Width != _combinedTexture.width || image.Height * 2 != _combinedTexture.height)
{
return true;
}
if (_combinedTexture.format != getTextureFormat(image))
{
return true;
}
return false;
}
public void Reconstruct(Image image, string globalShaderName, string pixelSizeName)
{
int combinedWidth = image.Width;
int combinedHeight = image.Height * 2;
TextureFormat format = getTextureFormat(image);
if (_combinedTexture != null)
{
DestroyImmediate(_combinedTexture);
}
_combinedTexture = new Texture2D(combinedWidth, combinedHeight, format, false, true);
_combinedTexture.wrapMode = TextureWrapMode.Clamp;
_combinedTexture.filterMode = FilterMode.Bilinear;
_combinedTexture.name = globalShaderName;
_combinedTexture.hideFlags = HideFlags.DontSave;
_intermediateArray = new byte[combinedWidth * combinedHeight * bytesPerPixel(format)];
Shader.SetGlobalTexture(globalShaderName, _combinedTexture);
Shader.SetGlobalVector(pixelSizeName, new Vector2(1.0f / image.Width, 1.0f / image.Height));
}
public void UpdateTexture(Image image)
{
_combinedTexture.LoadRawTextureData(image.Data(Image.CameraType.LEFT));
_combinedTexture.Apply();
}
private TextureFormat getTextureFormat(Image image)
{
switch (image.Format)
{
case Image.FormatType.INFRARED:
return TextureFormat.Alpha8;
default:
throw new Exception("Unexpected image format " + image.Format + "!");
}
}
private int bytesPerPixel(TextureFormat format)
{
switch (format)
{
case TextureFormat.Alpha8:
return 1;
default:
throw new Exception("Unexpected texture format " + format);
}
}
}
public class LeapDistortionData
{
private Texture2D _combinedTexture = null;
public Texture2D CombinedTexture
{
get
{
return _combinedTexture;
}
}
public bool CheckStale()
{
return _combinedTexture == null;
}
public void Reconstruct(Image image, string shaderName)
{
int combinedWidth = image.DistortionWidth / 2;
int combinedHeight = image.DistortionHeight * 2;
if (_combinedTexture != null)
{
DestroyImmediate(_combinedTexture);
}
Color32[] colorArray = new Color32[combinedWidth * combinedHeight];
_combinedTexture = new Texture2D(combinedWidth, combinedHeight, TextureFormat.RGBA32, false, true);
_combinedTexture.filterMode = FilterMode.Bilinear;
_combinedTexture.wrapMode = TextureWrapMode.Clamp;
_combinedTexture.hideFlags = HideFlags.DontSave;
addDistortionData(image, colorArray, 0);
_combinedTexture.SetPixels32(colorArray);
_combinedTexture.Apply();
Shader.SetGlobalTexture(shaderName, _combinedTexture);
}
private void addDistortionData(Image image, Color32[] colors, int startIndex)
{
float[] distortionData = image.Distortion(Image.CameraType.LEFT).
Query().
Concat(image.Distortion(Image.CameraType.RIGHT)).
ToArray();
for (int i = 0; i < distortionData.Length; i += 2)
{
byte b0, b1, b2, b3;
encodeFloat(distortionData[i], out b0, out b1);
encodeFloat(distortionData[i + 1], out b2, out b3);
colors[i / 2 + startIndex] = new Color32(b0, b1, b2, b3);
}
}
private void encodeFloat(float value, out byte byte0, out byte byte1)
{
// The distortion range is -0.6 to +1.7. Normalize to range [0..1).
value = (value + 0.6f) / 2.3f;
float enc_0 = value;
float enc_1 = value * 255.0f;
enc_0 = enc_0 - (int)enc_0;
enc_1 = enc_1 - (int)enc_1;
enc_0 -= 1.0f / 255.0f * enc_1;
byte0 = (byte)(enc_0 * 256.0f);
byte1 = (byte)(enc_1 * 256.0f);
}
}
public class EyeTextureData
{
private const string GLOBAL_RAW_TEXTURE_NAME = "_LeapGlobalRawTexture";
private const string GLOBAL_DISTORTION_TEXTURE_NAME = "_LeapGlobalDistortion";
private const string GLOBAL_RAW_PIXEL_SIZE_NAME = "_LeapGlobalRawPixelSize";
public readonly LeapTextureData TextureData;
public readonly LeapDistortionData Distortion;
private bool _isStale = false;
public static void ResetGlobalShaderValues()
{
Texture2D empty = new Texture2D(1, 1, TextureFormat.ARGB32, false, false);
empty.name = "EmptyTexture";
empty.hideFlags = HideFlags.DontSave;
empty.SetPixel(0, 0, new Color(0, 0, 0, 0));
Shader.SetGlobalTexture(GLOBAL_RAW_TEXTURE_NAME, empty);
Shader.SetGlobalTexture(GLOBAL_DISTORTION_TEXTURE_NAME, empty);
}
public EyeTextureData()
{
TextureData = new LeapTextureData();
Distortion = new LeapDistortionData();
}
public bool CheckStale(Image image)
{
return TextureData.CheckStale(image) ||
Distortion.CheckStale() ||
_isStale;
}
public void MarkStale()
{
_isStale = true;
}
public void Reconstruct(Image image)
{
TextureData.Reconstruct(image, GLOBAL_RAW_TEXTURE_NAME, GLOBAL_RAW_PIXEL_SIZE_NAME);
Distortion.Reconstruct(image, GLOBAL_DISTORTION_TEXTURE_NAME);
_isStale = false;
}
public void UpdateTextures(Image image)
{
TextureData.UpdateTexture(image);
}
}
#if UNITY_EDITOR
void OnValidate()
{
if (Application.isPlaying)
{
ApplyGammaCorrectionValues();
}
else
{
EyeTextureData.ResetGlobalShaderValues();
}
}
#endif
private void Awake()
{
if (_provider == null)
{
Debug.Log("Provider not assigned");
this.enabled = false;
return;
}
Camera.onPreRender -= OnCameraPreRender;
Camera.onPreRender += OnCameraPreRender;
//Enable pooling to reduce overhead of images
LeapInternal.MemoryManager.EnablePooling = true;
ApplyGammaCorrectionValues();
#if UNITY_2019_3_OR_NEWER
//SRP require subscribing to RenderPipelineManagers
if(UnityEngine.Rendering.GraphicsSettings.renderPipelineAsset != null) {
UnityEngine.Rendering.RenderPipelineManager.beginCameraRendering -= onBeginRendering;
UnityEngine.Rendering.RenderPipelineManager.beginCameraRendering += onBeginRendering;
}
#endif
}
private void OnEnable()
{
subscribeToService();
}
private void OnDisable()
{
unsubscribeFromService();
}
private void OnDestroy()
{
StopAllCoroutines();
Controller controller = _provider.GetLeapController();
if (controller != null)
{
controller.DistortionChange -= onDistortionChange;
controller.Disconnect -= onDisconnect;
controller.ImageReady -= onImageReady;
controller.FrameReady -= onFrameReady;
}
#if UNITY_2019_3_OR_NEWER
//SRP require subscribing to RenderPipelineManagers
if (UnityEngine.Rendering.GraphicsSettings.renderPipelineAsset != null)
{
UnityEngine.Rendering.RenderPipelineManager.beginCameraRendering -= onBeginRendering;
}
#endif
}
private void LateUpdate()
{
var xrProvider = _provider as LeapXRServiceProvider;
if (xrProvider != null)
{
if (xrProvider.mainCamera == null) { return; }
}
Frame imageFrame = _provider.CurrentFrame;
_currentImage = null;
if (_needQueueReset)
{
while (_imageQueue.TryDequeue()) { }
_needQueueReset = false;
}
/* Use the most recent image that is not newer than the current frame
* This means that the shown image might be slightly older than the current
* frame if for some reason a frame arrived before an image did.
*
* Usually however, this is just important when robust mode is enabled.
* At that time, image ids never line up with tracking ids.
*/
Image potentialImage;
while (_imageQueue.TryPeek(out potentialImage))
{
if (_frameIDOffset == -1) // Initialise to incoming image ID
{
_frameIDOffset = potentialImage.SequenceId + 1;
if (_frameIDOffset != 0)
{
Debug.LogWarning("Incoming image ID was " + potentialImage.SequenceId + " but we expected zero. Compensating..");
}
}
if (potentialImage.SequenceId > imageFrame.Id)
{
break;
}
_currentImage = potentialImage;
_imageQueue.TryDequeue();
}
}
private void OnCameraPreRender(Camera cam)
{
if (_currentImage != null)
{
if (_eyeTextureData.CheckStale(_currentImage))
{
_eyeTextureData.Reconstruct(_currentImage);
}
_eyeTextureData.UpdateTextures(_currentImage);
}
}
#if UNITY_2019_3_OR_NEWER
private void onBeginRendering(UnityEngine.Rendering.ScriptableRenderContext scriptableRenderContext, Camera camera)
{
OnCameraPreRender(camera);
}
#endif
private void subscribeToService()
{
if (_serviceCoroutine != null)
{
return;
}
_serviceCoroutine = StartCoroutine(serviceCoroutine());
}
private void unsubscribeFromService()
{
if (_serviceCoroutine != null)
{
StopCoroutine(_serviceCoroutine);
_serviceCoroutine = null;
}
var controller = _provider.GetLeapController();
if (controller != null)
{
controller.ClearPolicy(Controller.PolicyFlag.POLICY_IMAGES);
controller.Disconnect -= onDisconnect;
controller.ImageReady -= onImageReady;
controller.DistortionChange -= onDistortionChange;
controller.FrameReady -= onFrameReady;
}
_eyeTextureData.MarkStale();
}
private Coroutine _serviceCoroutine = null;
private IEnumerator serviceCoroutine()
{
Controller controller = null;
do
{
controller = _provider.GetLeapController();
yield return null;
} while (controller == null);
controller.FrameReady += onFrameReady;
controller.Disconnect += onDisconnect;
controller.ImageReady += onImageReady;
controller.DistortionChange += onDistortionChange;
}
private void onImageReady(object sender, ImageEventArgs args)
{
Image image = args.image;
if (!_imageQueue.TryEnqueue(image))
{
Debug.LogWarning("Image buffer filled up. This is unexpected and means images are being provided faster than " +
"LeapImageRetriever can consume them. This might happen if the application has stalled " +
"or we recieved a very high volume of images suddenly.");
_needQueueReset = true;
}
if (image.SequenceId < _prevSequenceId)
{
//We moved back in time, so we should reset the queue so it doesn't get stuck
//on the previous image, which will be very old.
//this typically happens when the service is restarted while the application is running.
_needQueueReset = true;
}
_prevSequenceId = image.SequenceId;
}
private void onFrameReady(object sender, FrameEventArgs args)
{
var controller = _provider.GetLeapController();
if (controller != null)
{
controller.FrameReady -= onFrameReady;
controller.SetPolicy(Controller.PolicyFlag.POLICY_IMAGES);
}
}
private void onDisconnect(object sender, ConnectionLostEventArgs args)
{
var controller = _provider.GetLeapController();
if (controller != null)
{
controller.FrameReady += onFrameReady;
controller.ClearPolicy(Controller.PolicyFlag.POLICY_IMAGES);
}
}
public void ApplyGammaCorrectionValues()
{
float gamma = 1f;
if (QualitySettings.activeColorSpace != ColorSpace.Linear)
{
gamma = -Mathf.Log10(Mathf.GammaToLinearSpace(0.1f));
}
Shader.SetGlobalFloat(GLOBAL_COLOR_SPACE_GAMMA_NAME, gamma);
Shader.SetGlobalFloat(GLOBAL_GAMMA_CORRECTION_EXPONENT_NAME, 1.0f / _gammaCorrection);
}
void onDistortionChange(object sender, LeapEventArgs args)
{
_eyeTextureData.MarkStale();
}
}
}