#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine.Formats.Alembic.Sdk;
using static UnityEngine.Formats.Alembic.Importer.RuntimeUtils;
namespace UnityEngine.Formats.Alembic.Importer
{
///
/// This component allows data streaming from Alembic files. It updates children nodes (Meshes, Transforms, Cameras, etc.) to reflect the Alembic data at the given time.
///
[ExecuteInEditMode]
[DisallowMultipleComponent]
public class AlembicStreamPlayer : MonoBehaviour, ISerializationCallbackReceiver
{
internal enum AlembicStreamSource
{
Internal = 0,
External = 1
}
[SerializeField] AlembicStreamSource streamSource = AlembicStreamSource.External;
internal AlembicStreamSource StreamSource
{
get => streamSource;
set => streamSource = value;
}
// "m_" prefix is intentionally missing and expose fields as public just to keep asset compatibility...
internal AlembicStream abcStream { get; set; }
[SerializeField] AlembicStreamDescriptor streamDescriptor;
[SerializeField] EmbeddedAlembicStreamDescriptor embeddedStreamDescriptor = new EmbeddedAlembicStreamDescriptor();
///
/// Gives access to the stream description.
///
internal IStreamDescriptor StreamDescriptor
{
get => StreamSource == AlembicStreamSource.External ? embeddedStreamDescriptor : (IStreamDescriptor)streamDescriptor;
set
{
if (StreamSource == AlembicStreamSource.External)
{
embeddedStreamDescriptor = (EmbeddedAlembicStreamDescriptor)value;
}
else
{
streamDescriptor = (AlembicStreamDescriptor)value;
}
}
}
[SerializeField]
float startTime = float.MinValue;
///
/// Get or set the start timestamp of the streaming time window (scale in seconds). This is clamped to the time range of the Alembic source file.
///
public float StartTime
{
get => startTime;
set
{
startTime = value;
if (StreamDescriptor != null)
{
startTime = Mathf.Clamp(startTime, StreamDescriptor.MediaStartTime, StreamDescriptor.MediaEndTime);
}
}
}
[SerializeField]
float endTime = float.MaxValue;
///
/// Get or set the end timestamp of the streaming time window (scale in seconds). This is clamped to the time range of the Alembic source file.
///
public float EndTime
{
get => endTime;
set
{
endTime = value;
if (StreamDescriptor != null)
{
endTime = Mathf.Clamp(endTime, StartTime, StreamDescriptor.MediaEndTime);
}
}
}
[SerializeField]
float currentTime;
///
/// Get or set the current time relative to the Alembic file time range (scale in seconds). This is clamped between 0 and the alembic time duration.
///
public float CurrentTime
{
get => currentTime;
set => currentTime = Mathf.Clamp(value, 0.0f, Duration);
}
///
/// Get the duration of the Alembic file (in seconds).
///
public float Duration => EndTime - StartTime;
[SerializeField]
float vertexMotionScale = 1.0f;
///
/// Get or set the scalar multiplier to the Alembic vertex speed (magnification factor for velocity). Default value is 1.
///
public float VertexMotionScale
{
get => vertexMotionScale;
set => vertexMotionScale = value;
}
///
/// The start timestamp of the Alembic file (scale in seconds).
///
public float MediaStartTime => StreamDescriptor != null ? StreamDescriptor.MediaStartTime : 0;
///
/// The end timestamp of the Alembic file (scale in seconds).
///
public float MediaEndTime => StreamDescriptor != null ? StreamDescriptor.MediaEndTime : 0;
///
/// The duration of the Alembic file (in seconds).
///
public float MediaDuration => MediaEndTime - MediaStartTime;
///
/// The path to the Alembic asset. When in a standalone build, the returned path is prepended by the streamingAssets path.
///
public string PathToAbc => StreamDescriptor != null ? StreamDescriptor.PathToAbc : "";
///
/// The stream import options. NOTE: these options are shared between all instances of this asset.
///
public AlembicStreamSettings Settings
{
get { return StreamDescriptor != null ? StreamDescriptor.Settings : null; }
set
{
if (StreamDescriptor == null)
{
StreamDescriptor = ScriptableObject.CreateInstance();
}
StreamDescriptor.Settings = value;
ReloadStream();
}
}
float lastUpdateTime;
bool forceUpdate = false;
bool updateStarted = false;
///
/// Update the child GameObject's data to the CurrentTime (The regular update happens during the LateUpdate phase).
///
/// The timestamp to stream from the asset file.
public void UpdateImmediately(float time)
{
CurrentTime = time;
Update();
LateUpdate();
}
///
/// Loads a different Alembic file.
///
/// Path to the new file.
/// True if the load succeeded, false otherwise.
public bool LoadFromFile(string newPath)
{
AlembicStreamAnalytics.SendAnalytics();
if (StreamDescriptor == null)
{
StreamDescriptor = ScriptableObject.CreateInstance();
}
StreamDescriptor.PathToAbc = newPath;
return InitializeAfterLoad();
}
///
/// Closes and reopens the Alembic stream. Use this method to apply the new stream settings.
///
/// If true, it also recreates the missing GameObjects for the Alembic nodes. >
/// True if the stream was successfully reopened, false otherwise.>
public bool ReloadStream(bool createMissingNodes = false)
{
if (abcStream != null)
{
abcStream?.Dispose();
return LoadStream(createMissingNodes);
}
return true;
}
///
/// This function removes all child game objects that don't have a corresponding alembic node. Note that is the object is a part of a prefab, this call will fail. Please note that GameObjects that are a part of a Prefab cannot be deleted.
///
public void RemoveObsoleteGameObjects()
{
ReloadStream(true);
RemoveObsoleteGameObject(gameObject);
}
void RemoveObsoleteGameObject(GameObject root)
{
var nChildren = root.transform.childCount;
for (var i = nChildren - 1; i >= 0; --i) // need to iterate backwards because deleting an object changes the child count
{
RemoveObsoleteGameObject(root.transform.GetChild(i).gameObject);
}
if (abcStream.abcTreeRoot.FindNode(root) == null) // no alembic node means not driven by ABC
{
#if UNITY_EDITOR
var prefabInstanceStatus = PrefabUtility.GetPrefabInstanceStatus(root);
if (prefabInstanceStatus == PrefabInstanceStatus.Connected)
{
Debug.LogError($"Cannot Remove GameObject: {root.name} because it is a part of a Prefab. Please delete in Prefab Isolation Mode");
return;
}
#endif
DestroyUnityObject(root);
}
}
bool InitializeAfterLoad()
{
var ret = LoadStream(true, true);
if (!ret)
return false;
abcStream.GetTimeRange(out var start, out var end);
startTime = (float)start;
endTime = (float)end;
StreamDescriptor.MediaStartTime = (float)start;
StreamDescriptor.MediaEndTime = (float)end;
var defaultMat = AlembicMesh.GetDefaultMaterial();
gameObject.DepthFirstVisitor(go =>
{
var filter = go.GetComponent();
var meshRenderer = go.GetComponent();
if (filter == null || meshRenderer == null)
{
return;
}
var mesh = filter.sharedMesh;
if (mesh == null)
{
return;
}
var subMesh = mesh.subMeshCount;
var newMats = new Material[subMesh];
var oldMats = meshRenderer.sharedMaterials;
for (var i = 0; i < subMesh; ++i)
{
newMats[i] = i < oldMats.Length && oldMats[i] != null ? oldMats[i] : defaultMat;
}
meshRenderer.sharedMaterials = newMats;
});
return true;
}
internal void CloseStream()
{
abcStream?.Dispose();
abcStream = null;
}
void ClampTime()
{
CurrentTime = Mathf.Clamp(CurrentTime, 0.0f, Duration);
}
internal bool LoadStream(bool createMissingNodes, bool serializeMesh = false)
{
if (StreamDescriptor == null || string.IsNullOrEmpty(StreamDescriptor.PathToAbc))
return false;
CloseStream();
abcStream = new AlembicStream(gameObject, StreamDescriptor);
var ret = abcStream.AbcLoad(createMissingNodes, serializeMesh);
forceUpdate = true;
return ret;
}
void Start()
{
OnValidate();
}
void OnValidate()
{
if (StreamDescriptor == null || abcStream == null)
return;
if (StreamDescriptor.MediaStartTime == double.MinValue || StreamDescriptor.MediaEndTime == double.MaxValue)
{
double start, end;
abcStream.GetTimeRange(out start, out end);
StreamDescriptor.MediaStartTime = (float)start;
StreamDescriptor.MediaEndTime = (float)end;
}
StartTime = Mathf.Clamp(StartTime, StreamDescriptor.MediaStartTime, StreamDescriptor.MediaEndTime);
EndTime = Mathf.Clamp(EndTime, StartTime, StreamDescriptor.MediaEndTime);
ClampTime();
forceUpdate = true;
}
internal void Update()
{
if (abcStream == null || StreamDescriptor == null)
return;
ClampTime();
if (lastUpdateTime != CurrentTime || forceUpdate)
{
abcStream.SetVertexMotionScale(VertexMotionScale);
if (abcStream.AbcUpdateBegin(StartTime + CurrentTime))
{
lastUpdateTime = CurrentTime;
forceUpdate = false;
updateStarted = true;
}
else
{
CloseStream();
LoadStream(false);
}
}
}
void LateUpdate()
{
// currentTime maybe updated after Update() by other GameObjects
if (!updateStarted && lastUpdateTime != currentTime)
Update();
if (!updateStarted && abcStream != null)
{
// If the model did not move this frame, we need to clear the motion vectors to avoid post processing artefacts.
abcStream.ClearMotionVectors();
return;
}
updateStarted = false;
if (abcStream != null)
{
abcStream.AbcUpdateEnd();
}
}
void OnEnable()
{
if (abcStream == null)
LoadStream(false);
}
void OnDisable()
{
CloseStream();
}
void OnApplicationQuit()
{
NativeMethods.aiCleanup();
}
///
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
}
///
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
if (streamDescriptor != null && streamDescriptor.GetType() == typeof(AlembicStreamDescriptor))
{
streamSource = AlembicStreamSource.Internal;
}
}
}
}